5.4. C/C++ 语言扩展
5.4.1. 函数和变量注解
5.4.1.1. 执行空间说明符
执行空间说明符__host__、__device__、__tile__、__global__和__tile_global__指明函数是在主机、SIMT 还是分块(tile)上下文中执行。
执行空间说明符 |
执行所在位置 |
可从何处调用 |
||||
|---|---|---|---|---|---|---|
主机 |
SIMT |
分块 |
主机 |
SIMT |
分块 |
|
|
✅ |
❌ |
❌ |
✅ |
❌ |
❌ |
|
❌ |
✅ |
❌ |
❌ |
✅ |
❌ |
|
❌ |
✅ |
❌ |
✅ |
✅ |
❌ |
|
❌ |
❌ |
✅ |
❌ |
❌ |
✅ |
|
❌ |
❌ |
✅ |
✅ |
❌ |
❌ |
|
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
__global__和__tile_global__函数的约束:
对__global__和__tile_global__函数的调用是异步的。它们会在设备完成执行之前返回到主机线程。
声明为具有多个执行空间的函数(例如__host__ __device__)会针对每个上下文进行编译。可以使用__CUDA_ARCH__宏来区分主机代码路径和设备代码路径:
__host__ __device__ void func() {
#if defined(__CUDA_ARCH__)
// Device code path
#else
// Host code path
#endif
}
5.4.1.2. 内存空间说明符
内存空间说明符__device__、__tile__、__managed__、__constant__和__shared__指明变量在设备上的存储位置。
下表汇总了内存空间的属性:
内存空间说明符 |
位置 |
可由何者访问 |
生存期 |
唯一实例 |
|---|---|---|---|---|
|
设备全局内存 |
设备线程(网格)/ CUDA Runtime API |
程序/CUDA 上下文 |
每个设备 |
|
设备全局内存 |
分块块 / CUDA Runtime API |
程序/CUDA 上下文 |
每个设备 |
|
设备常量内存 |
设备线程(网格)/ CUDA Runtime API |
程序/CUDA 上下文 |
每个设备 |
|
主机和设备(自动) |
主机/设备线程 |
程序 |
每个程序 |
|
设备(流式多处理器(SM)) |
线程块中的线程 |
线程块 |
线程块 |
无说明符 |
设备(寄存器) |
单个线程 |
单个线程 |
单个线程 |
可以使用CUDA Runtime API函数
cudaGetSymbolAddress()、cudaGetSymbolSize()、cudaMemcpyToSymbol()和cudaMemcpyFromSymbol(),从主机访问__device__、__tile__和__constant__变量。__constant__变量在设备代码中是只读的,只能在主机上使用CUDA Runtime API进行修改。
以下示例说明了如何使用这些 API:
__device__ float device_var = 4.0f; // Variable in device memory
__constant__ float constant_mem_var = 4.0f; // Variable in constant memory
// For readability, the following example focuses on a device variable.
int main() {
float* device_ptr;
cudaGetSymbolAddress((void**) &device_ptr, device_var); // Gets address of device_var
size_t symbol_size;
cudaGetSymbolSize(&symbol_size, device_var); // Retrieves the size of the symbol (4 bytes).
float host_var;
cudaMemcpyFromSymbol(&host_var, device_var, sizeof(host_var)); // Copies from device to host.
host_var = 3.0f;
cudaMemcpyToSymbol(device_var, &host_var, sizeof(host_var)); // Copies from host to device.
}
请参阅Compiler Explorer上的示例。
5.4.1.2.2. __managed__内存
__managed__变量具有以下限制:
__managed__变量的地址不是常量表达式。__managed__变量不得具有引用类型T&。当 CUDA 运行时可能处于无效状态时,不得使用
__managed__变量的地址或值,包括以下情况:在对具有
static或thread_local存储期的对象进行静态/动态初始化或销毁时。在调用
exit()之后执行的代码中。例如,带有__attribute__((destructor))标记的函数。在 CUDA 运行时可能尚未初始化时执行的代码中。例如,带有
__attribute__((constructor))标记的函数。
__managed__变量不能用作decltype()表达式的不带括号的标识符表达式实参。__managed__变量具有与动态分配的托管内存所规定的相同的相干性和一致性行为。另请参阅对局部变量的限制。
以下是__managed__变量的合法用法和非法用法示例:
#include <cassert>
__device__ __managed__ int global_var = 10; // OK
int* ptr = &global_var; // ERROR: use of a managed variable in static initialization
struct MyStruct1 {
int field;
MyStruct1() : field(global_var) {};
};
struct MyStruct2 {
~MyStruct2() { global_var = 10; }
};
MyStruct1 temp1; // ERROR: use of managed variable in dynamic initialization
MyStruct2 temp2; // ERROR: use of managed variable in the destructor of
// object with static storage duration
__device__ __managed__ const int const_var = 10; // ERROR: const-qualified type
__device__ __managed__ int& reference = global_var; // ERROR: reference type
template <int* Addr>
struct MyStruct3 {};
MyStruct3<&global_var> temp; // ERROR: address of managed variable is not a constant expression
__global__ void kernel(int* ptr) {
assert(ptr == &global_var); // OK
global_var = 20; // OK
}
int main() {
int* ptr = &global_var; // OK
kernel<<<1, 1>>>(ptr);
cudaDeviceSynchronize();
global_var++; // OK
decltype(global_var) var1; // ERROR: managed variable used as unparenthesized argument to decltype
decltype((global_var)) var2; // OK
}
5.4.1.2.3. __tile__变量
__tile__内存空间说明符类似于__device__。带有__tile__标记的变量分配在设备全局内存中,并可由 CUDA Runtime API 函数访问。与__device__变量不同,__tile__变量可以从分块代码中直接访问。
一般而言,不能从__tile__或__tile_global__代码中通过名称直接访问__device__变量,也不能从__device__或__global__代码中通过名称直接访问__tile__变量。但是,这两类变量都分配在设备全局内存中,指向该内存的指针可以在任一上下文中访问。
以下示例展示了分块变量的合法用法和非法用法:
__device__ int x_device = 0;
__tile__ int x_tile = 0;
// ERROR: Variable cannot be both device and tile
__device__ __tile__ int x_device_tile = 0;
__global__ void simt_function(int* in) {
x_device = 1; // OK: __device__ variable accessed from SIMT code
x_tile = 1; // ERROR: __tile__ variable accessed from SIMT code
*in = 1; // OK: May point to any device global memory, including x_tile.
}
__tile_global__ void tile_function(int* in) {
x_device = 1; // ERROR: __device__ variable accessed from tile code
x_tile = 1; // OK: __tile__ variable accessed from tile code
*in = 1; // OK: May point to any device global memory, including x_device.
}
int main() {
int* x_device_ptr;
cudaGetSymbolAddress((void**) &x_device_ptr, x_device); // Gets address of x_device
int* x_tile_ptr;
cudaGetSymbolAddress((void**) &x_tile_ptr, x_tile); // Gets address of x_tile
// OK: Passing pointer to tile variable into SIMT kernel
simt_function<<<1,1>>>(x_tile_ptr);
cudaDeviceSynchronize();
// OK: Passing pointer to device variable into tile kernel
tile_function<<<1,1>>>(x_device_ptr);
cudaDeviceSynchronize();
}
__tile__变量不得包含指针类型或引用类型的子对象。例如:
__tile__ int* ptr; // ERROR
struct S1 { int* ptr; };
__tile__ S1 val; // ERROR
5.4.1.3. 内联说明符
以下说明符可用于控制__host__和__device__函数的内联:
__noinline__:指示nvcc不要内联该函数。__forceinline__:强制nvcc在单个翻译单元内内联该函数。__inline_hint__:在使用链接时优化时,启用跨翻译单元的激进内联。
这些说明符互斥。将这些说明符应用于__tile__函数时,它们会被忽略。
5.4.1.4. __restrict__指针
nvcc通过__restrict__关键字支持受限指针。
当两个或更多指针指向重叠的内存区域时,就会发生指针别名。这可能妨碍代码重排和公共子表达式消除等优化。
用 restrict 限定的指针是程序员作出的一项承诺:在该指针的生存期内,只会通过该指针访问它所指向的内存。这使编译器能够执行更激进的优化。
访问设备函数的所有线程都只从中读取;或者
最多只有一个线程向其写入,并且没有其他线程从中读取。
以下示例说明了一个别名问题,并展示了使用受限指针如何帮助编译器减少指令数量:
__device__
void device_function(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];
...
}
由于指针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的元素。这会将函数原型更改为如下形式:
__device__
void device_function(const float* __restrict__ a, const float* __restrict__ b, float* __restrict__ c);
请注意,所有指针实参都必须是受限的,编译器优化器才能发挥作用。添加__restrict__关键字后,编译器可以任意进行重排和公共子表达式消除,同时保持与抽象执行模型完全相同的功能。
__device__
void device_function(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;
...
}
请参阅Compiler Explorer上的示例。
其结果是内存访问和计算次数减少,但代价是将加载的值和公共子表达式缓存在寄存器中会增加寄存器压力。
由于寄存器压力在许多 CUDA 代码中是一个关键问题,使用受限指针可能会因降低占用率而对性能产生负面影响。
对__global__函数中带有__restrict__标记的const指针的访问会被编译为只读缓存加载,类似于PTX的ld.global.nc或__ldg()低级加载和存储函数指令。
__global__
void kernel1(const float* in, float* out) {
*out = *in; // PTX: ld.global
}
__global__
void kernel2(const float* __restrict__ in, float* out) {
*out = *in; // PTX: ld.global.nc
}
请参阅Compiler Explorer上的示例。
5.4.1.5. __grid_constant__参数
使用__grid_constant__注解__global__函数参数可防止编译器为每个线程创建该参数的副本。相反,网格中的所有线程都将通过同一地址访问该参数,这可以提高性能。
__grid_constant__参数具有以下属性:
它的生存期与核函数相同。
它是单个核函数私有的,这意味着其他网格(包括子网格)中的线程无法访问该对象。
核函数中的所有线程看到的地址都相同。
它是只读的。修改
__grid_constant__对象或其任何子对象(包括mutable成员)属于未定义行为。
要求:
使用
__grid_constant__注解的核函数参数必须具有const限定的非引用类型。所有函数声明在任何
__grid_constant__参数上都必须保持一致。函数模板特化在任何
__grid_constant__参数上都必须与主模板声明匹配。函数模板实例化在任何
__grid_constant__参数上也必须与主模板声明匹配。__tile_global__函数参数中的__grid_constant__注解会被忽略。
示例:
struct MyStruct {
int x;
mutable int y;
};
__device__ void external_function(const MyStruct&);
__global__ void kernel(const __grid_constant__ MyStruct s) {
// s.x++; // Compile error: tried to modify read-only memory
// s.y++; // Undefined Behavior: tried to modify read-only memory
// Compiler will NOT create a per-thread local copy of "s":
external_function(s);
}
请参阅Compiler Explorer上的示例。
5.4.1.6. 注解汇总
下表汇总了 CUDA 注解,并说明每个注解适用于哪个执行空间以及在何处有效。
注解 |
|
|
|---|---|---|
函数 |
❌ |
|
指针参数 |
指针参数 |
|
❌ |
参数 |
|
❌ |
函数 |
|
❌ |
函数 |
|
❌ |
函数 |
5.4.2. 内建类型和变量
5.4.2.1. 主机编译器类型扩展
只要主机编译器支持,CUDA 就允许使用非标准算术类型。支持以下类型:
128 位整数类型
__int128。当主机编译器定义了
__SIZEOF_INT128__宏时,在 Linux 上受支持。
128 位浮点类型
__float128和_Float128可用于计算能力为 10.0 及更高版本的 GPU 设备。编译器可能使用精度较低的浮点表示来处理__float128类型的常量表达式。当主机编译器定义了
__SIZEOF_FLOAT128__或__FLOAT128__宏时,在 Linux x86 上受支持。
_Complex类型仅在主机代码中受支持。
分块代码不支持 128 位整数类型和 128 位浮点类型。
5.4.2.2. 内建变量
用于指定和获取网格及线程块沿 x、y 和 z 维度的核函数配置的值,其类型为dim3。用于获取线程块索引和线程索引的变量,其类型为uint3。dim3和uint3都是平凡结构,由三个分别名为x、y和z的无符号值组成。在 C++11 及更高版本中,dim3的所有分量的默认值均为 1。
仅限设备使用的内建变量:
dim3 gridDim:包含网格在 x、y 和 z 维度上的大小,即线程块数量。dim3 blockDim:包含线程块在 x、y 和 z 维度上的大小,即线程数量。uint3 blockIdx:包含线程块在网格中沿 x、y 和 z 维度的索引。uint3 threadIdx:包含线程在线程块中沿 x、y 和 z 维度的索引。int warpSize:一个运行时值,定义为一个线程束中的线程数量,通常为32。有关线程束的定义,另请参阅线程束与 SIMT。
分块代码不支持这些变量。要在分块代码中获取块 ID 或网格大小,请使用cuda::tiles::bid()和cuda::tiles::num_blocks()API。
5.4.2.3. 内建类型
CUDA 提供了从基本整数类型和浮点类型派生的向量类型,主机和设备均支持这些类型。下表列出了可用的向量类型。
C++ 基本类型 |
向量 X1 |
向量 X2 |
向量 X3 |
向量 X4 |
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
请注意,long4、ulong4、longlong4、ulonglong4和double4在 CUDA 13 中已被弃用,并且可能在未来的版本中移除。
下表详细列出了向量类型的字节大小和对齐要求:
类型 |
大小 |
对齐 |
|---|---|---|
|
1 |
1 |
|
2 |
2 |
|
3 |
1 |
|
4 |
4 |
|
2 |
2 |
|
4 |
4 |
|
6 |
2 |
|
8 |
8 |
|
4 |
4 |
|
8 |
8 |
|
12 |
4 |
|
16 |
16 |
|
4/8 * |
4/8 * |
|
8/16 * |
8/16 * |
|
12/24 * |
4/8 * |
|
16/32 * |
16 * |
|
16/32 * |
16 |
|
16/32 * |
32 |
|
8 |
8 |
|
16 |
16 |
|
24 |
8 |
|
32 |
16 |
|
32 |
16 |
|
32 |
32 |
|
4 |
4 |
|
8 |
8 |
|
12 |
4 |
|
16 |
16 |
|
8 |
8 |
|
16 |
16 |
|
24 |
8 |
|
32 |
16 |
|
32 |
16 |
|
32 |
32 |
*long在 C++ LLP64 数据模型(Windows 64 位)中为 4 字节,而在 C++ LP64 数据模型(Linux 64 位)中为 8 字节。
向量类型是结构体。其第一个、第二个、第三个和第四个分量分别可通过x、y、z和w字段访问。
int sum(int4 value) {
return value.x + value.y + value.z + value.w;
}
它们都有一个形式为make_<type_name>()的工厂函数;例如:
int4 add_one(int x, int y, int z, int w) {
return make_int4(x + 1, y + 1, z + 1, w + 1);
}
如果主机代码不是使用nvcc编译的,则可以通过包含 CUDA 工具包提供的cuda_runtime.h头文件来导入向量类型及相关函数。
5.4.3. 核函数配置
对__global__或__tile_global__函数的任何调用都必须为该调用指定一个执行配置。此执行配置定义了在设备上执行该函数所使用的网格和线程块的维度,以及关联的流。
执行配置通过在函数名与括号括起的参数列表之间插入形式为<<<grid_dim, block_dim, dynamic_smem_bytes, stream>>>的表达式来指定,其中:
grid_dim的类型为dim3,用于指定网格的维度和大小,使grid_dim.x * grid_dim.y * grid_dim.z等于所启动的线程块数量。block_dim的类型为dim3,用于指定每个线程块的维度和大小,使block_dim.x * block_dim.y * block_dim.z等于每个线程块的线程数量。对于__tile_global__核函数,block_dim必须为1,因为编译器将决定启动分块核函数时要调度多少个线程。dynamic_smem_bytes是一个可选的size_t参数,默认值为零。它指定此次调用中,除静态分配的内存之外,为每个线程块动态分配的共享内存字节数。此内存由extern __shared__数组使用(请参阅__shared__ 内存)。stream的类型为cudaStream_t(指针),用于指定关联的流。stream是一个可选参数,默认值为NULL。
以下示例展示了核函数的声明和调用:
__global__ void kernel(float* parameter);
kernel<<<grid_dim, block_dim, dynamic_smem_bytes>>>(parameter);
执行配置的参数先于实际函数的参数求值。
如果grid_dim或block_dim超过了设备所允许的最大大小(如计算能力中所述),或者dynamic_smem_bytes大于扣除静态分配的内存之后可用的共享内存,则函数调用将失败。
5.4.3.1. 线程块簇
计算能力 9.0 及更高版本允许用户指定编译时线程块簇维度,以便核函数能够使用 CUDA 中的簇层次结构。编译时簇维度可以使用__cluster_dims__属性按以下语法指定:__cluster_dims__([x, [y, [z]]])。下面的示例展示了编译时簇大小,其中 X 维度为 2,Y 和 Z 维度为 1。
__global__ void __cluster_dims__(2, 1, 1) kernel(float* parameter);
__cluster_dims__()的默认形式指定核函数将作为网格簇启动。如果未指定簇维度,用户可以在启动时指定。如果在启动时未指定维度,将导致启动时错误。不能在__tile_global__核函数上指定__cluster_dims__属性,因为编译器会决定如何跨簇调度分块核函数。
线程块簇的维度也可以在运行时指定,并且可以使用cudaLaunchKernelEx API 启动带有簇的核函数。此 API 接受一个类型为cudaLaunchConfig_t的配置参数、一个核函数指针和核函数参数。下面的示例展示了运行时核函数配置。
__global__ void kernel(float parameter1, int parameter2) {}
int main() {
cudaLaunchConfig_t config = {0};
// The grid dimension is not affected by cluster launch, and is still enumerated
// using the number of blocks.
// The grid dimension should be a multiple of cluster size.
config.gridDim = dim3{4}; // 4 blocks
config.blockDim = dim3{32}; // 32 threads per block
config.dynamicSmemBytes = 1024; // 1 KB
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 parameter1 = 3.0f;
int parameter2 = 4;
cudaLaunchKernelEx(&config, kernel, parameter1, parameter2);
}
请参阅Compiler Explorer上的示例。
5.4.3.2. 启动边界
如核函数启动与占用率一节所述,使用较少的寄存器可以使更多线程和线程块驻留在一个多处理器上,从而提高性能。
因此,编译器使用启发式方法,在将寄存器溢出和指令数量保持在最低水平的同时,尽量减少寄存器使用量。应用程序可以选择以启动边界的形式向编译器提供附加信息,以辅助这些启发式方法;启动边界通过在__global__函数的定义中使用__launch_bounds__()限定符来指定:
__global__ void
__launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor, maxBlocksPerCluster)
MyKernel(...) {
...
}
maxThreadsPerBlock指定应用程序在启动MyKernel()时所使用的每个线程块的最大线程数;它会被编译为.maxntidPTX 编译指示。minBlocksPerMultiprocessor是可选的,用于指定期望的每个多处理器上驻留线程块的最小数量;它会被编译为.minnctapersmPTX 编译指示。maxBlocksPerCluster是可选的,用于指定应用程序在启动MyKernel()时所期望使用的每个簇的最大线程块数;它会被编译为.maxclusterrankPTX 编译指示。
如果指定了启动边界,编译器首先会推导出核函数应使用的寄存器数量上限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__ >= 900
#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
__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__在主机代码中未定义。因此,MyKernel将以每个线程块 256 个线程启动。每个线程块的线程数应改为通过以下方式确定:
在编译时使用不依赖于
__CUDA_ARCH__的宏或常量,例如// Host code MyKernel<<<blocksPerGrid, THREADS_PER_BLOCK>>>(...);
或者在运行时根据计算能力确定
// Host code cudaGetDeviceProperties(&deviceProp, device); int threadsPerBlock = (deviceProp.major >= 9) ? 2 * THREADS_PER_BLOCK : THREADS_PER_BLOCK; MyKernel<<<blocksPerGrid, threadsPerBlock>>>(...);
--resource-usage编译器选项会报告寄存器使用量。CUDA 性能分析器会报告占用率,可以据此推导出驻留线程块的数量。
5.4.3.3. 每个线程的最大寄存器数量
为支持底层性能调优,CUDA C++ 提供了__maxnreg__()函数限定符,用于将性能调优信息传递给后端优化编译器。__maxnreg__()限定符指定可分配给线程块中单个线程的最大寄存器数量。在__global__函数的定义中:
__global__ void
__maxnreg__(maxNumberRegistersPerThread)
MyKernel(...) {
...
}
maxNumberRegistersPerThread变量指定要分配给核函数MyKernel()的线程块中单个线程的最大寄存器数量;它会被编译为.maxnreg PTX 编译指示。
__launch_bounds__()和__maxnreg__()限定符不能同时应用于同一个核函数。
--maxrregcount <N>编译器选项可用于控制文件中所有__global__函数的寄存器使用量。对于带有__maxnreg__限定符的核函数,此选项会被忽略。
5.4.4. 同步原语
5.4.4.1. 线程块同步函数
void __syncthreads();
int __syncthreads_count(int predicate);
int __syncthreads_and(int predicate);
int __syncthreads_or(int predicate);
这些内建函数协调同一个线程块内线程之间的通信。当线程块中的线程访问共享内存或全局内存中的相同地址时,可能会发生写后读、读后写或写后写冒险。通过在此类访问之间同步线程,可以避免这些冒险。
这些内建函数具有以下语义:
__syncthreads*()会等待,直到线程块中所有尚未退出的线程同时到达程序中的同一个__syncthreads*()内建函数调用,或退出。__syncthreads*()在参与线程之间提供内存排序:对__syncthreads*()内建函数的调用强先发生于(请参阅C++ 规范 [intro.races])任何参与线程解除等待阻塞或退出。
以下示例展示了如何使用__syncthreads()同步线程块内的线程,并安全地对线程间共享的数组元素求和:
#include <cuda_runtime_api.h>
#include <memory.h>
#include <cstdlib>
#include <ctime>
#include <stdio.h>
// assuming blockDim.x is 128
__global__ void example_syncthreads(int* input_data, int* output_data)
{
__shared__ int shared_data[128];
shared_data[threadIdx.x] = input_data[blockDim.x*blockIdx.x + threadIdx.x];
// All threads synchronize, guaranteeing all writes to 'shared_data' are ordered
// before any thread is unblocked from '__syncthreads()':
__syncthreads();
// A single thread safely reads 'shared_data':
if (threadIdx.x == 0) {
float sum = 0;
for (int i = 0; i < blockDim.x; ++i) {
sum += shared_data[i];
}
output_data[blockIdx.x] = sum;
}
}
void initArray(int* A, int length)
{
std::srand(std::time({}));
for(int i=0; i<length; i++)
{
A[i] = int(10.f * (rand() / (float)RAND_MAX));
}
}
int main(int argc, char** arg)
{
constexpr int block_size = 128;
constexpr int input_length = 1024;
constexpr int output_length = input_length / block_size;
// Pointers to memory vectors
int* input = nullptr;
int* output = nullptr;
int* comparisonResult = (int*)malloc(output_length*sizeof(int));
// Use unified memory to allocate buffers
cudaMallocManaged(&input, input_length*sizeof(int));
cudaMallocManaged(&output, output_length*sizeof(int));
initArray(input, input_length);
int grid_size = input_length / block_size;
example_syncthreads<<<grid_size, block_size>>>(input, output);
cudaDeviceSynchronize();
for(int i=0; i<output_length; i++)
{
comparisonResult[i] = 0;
for(int j=0; j < block_size; j++)
{
comparisonResult[i] += input[i*block_size + j];
}
}
for(int i=0; i< output_length; i++)
{
if(output[i] != comparisonResult[i])
{
printf("Results do not match at index %d: %d != %d\n", i, output[i], comparisonResult[i]);
exit(-1);
}
}
printf("Test passed\n");
return 0;
}
允许在条件代码中使用__syncthreads*()内建函数,但前提是整个线程块对该条件的求值结果一致。否则,执行可能会挂起或产生意外的副作用。
以下示例展示了一种有效行为:
// assuming blockDim.x is 128
__global__ void syncthreads_valid_behavior(int* input_data, int* output_data) {
__shared__ int shared_data[128];
shared_data[threadIdx.x] = input_data[threadIdx.x];
if (blockIdx.x > 0) { // CORRECT, uniform condition across all block threads
__syncthreads();
output_data[threadIdx.x] = shared_data[128 - threadIdx.x];
}
}
而以下示例则表现出无效行为,例如核函数挂起,或未定义行为:
// assuming blockDim.x is 128
__global__ void syncthreads_invalid_behavior1(int* input_data, int* output_data) {
__shared__ int shared_data[256];
shared_data[threadIdx.x] = input_data[threadIdx.x];
if (threadIdx.x > 0) { // WRONG, non-uniform condition
__syncthreads(); // Undefined Behavior
output_data[threadIdx.x] = shared_data[128 - threadIdx.x];
}
}
// assuming blockDim.x is 128
__global__ void syncthreads_invalid_behavior2(int* input_data, int* output_data) {
__shared__ int shared_data[256];
shared_data[threadIdx.x] = input_data[threadIdx.x];
for (int i = 0; i < blockDim.x; ++i) {
if (i == threadIdx.x) { // WRONG, non-uniform condition
__syncthreads(); // Undefined Behavior
}
}
output_data[threadIdx.x] = shared_data[128 - threadIdx.x];
}
__syncthreads()带谓词的变体:
int __syncthreads_count(int predicate);
与__syncthreads()相同,不同之处在于,它会对线程块中所有尚未退出的线程求值一个谓词,并返回谓词求值结果为非零值的线程数量。
int __syncthreads_and(int predicate);
与__syncthreads()相同,不同之处在于,它会对线程块中所有尚未退出的线程求值该谓词。当且仅当所有这些线程的谓词求值结果均为非零值时,它才返回非零值。
int __syncthreads_or(int predicate);
与__syncthreads()相同,不同之处在于,它会对线程块中所有尚未退出的线程求值该谓词。当且仅当其中一个或多个线程的谓词求值结果为非零值时,它才返回非零值。
5.4.4.2. 线程束同步函数
void __syncwarp(unsigned mask = 0xFFFFFFFF);
内建函数__syncwarp()协调同一个线程束内线程之间的通信。当线程束中的某些线程访问共享内存或全局内存中的相同地址时,可能会发生潜在的写后读、读后写或写后写冒险。通过在这些访问之间同步线程,可以避免这些数据冒险。
调用__syncwarp(mask)会在一个线程束内由mask指定的参与线程之间提供内存排序:对__syncwarp(mask)的调用强先发生于(请参阅C++ 规范 [intro.races])由mask指定的任何线程束线程解除等待阻塞或退出。
这些函数受线程束 __sync 内建函数约束的限制。
以下示例展示了如何使用__syncwarp()同步线程束内的线程,以安全地访问共享内存数组:
__global__ void example_syncwarp(int* input_data, int* output_data) {
if (threadIdx.x < warpSize) {
__shared__ int shared_data[warpSize];
shared_data[threadIdx.x] = input_data[threadIdx.x];
__syncwarp(); // equivalent to __syncwarp(0xFFFFFFFF)
if (threadIdx.x == 0)
output_data[0] = shared_data[1];
}
}
5.4.4.3. 内存栅栏函数
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的结果值可以是任意值。
内存栅栏和同步函数强制内存访问采用顺序一致的排序。这些函数的区别在于强制执行排序的线程作用域,但与所访问的内存空间无关,这些内存空间包括共享内存、全局内存、页锁定主机内存以及对等设备的内存。
提示
出于安全性和可移植性考虑,建议尽可能使用libcu++提供的cuda::atomic_thread_fence。
线程块级内存栅栏
// <cuda/atomic> header
cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_block);
确保:
调用线程在调用
cuda::atomic_thread_fence()之前对所有内存进行的所有写入,都会被该调用线程所在的线程块中的所有线程观察为发生在该调用线程在调用cuda::atomic_thread_fence()之后对所有内存进行的所有写入之前;调用线程在调用
cuda::atomic_thread_fence()之前从所有内存进行的所有读取,都排在该调用线程在调用cuda::atomic_thread_fence()之后从所有内存进行的所有读取之前。
void __threadfence_block();
确保:
调用线程在调用
__threadfence_block()之前对所有内存进行的所有写入,都会被该调用线程所在的线程块中的所有线程观察为发生在该调用线程在调用__threadfence_block()之后对所有内存进行的所有写入之前;调用线程在调用
__threadfence_block()之前从所有内存进行的所有读取,都排在该调用线程在调用__threadfence_block()之后从所有内存进行的所有读取之前。
设备级内存栅栏
cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_device);
确保:
对于调用线程在调用
cuda::atomic_thread_fence()之后对所有内存进行的任何写入,设备中的任何线程都不会将其观察为发生在调用线程在调用cuda::atomic_thread_fence()之前对所有内存进行的任何写入之前。
void __threadfence();
确保:
对于调用线程在调用
__threadfence()之后对所有内存进行的任何写入,设备中的任何线程都不会将其观察为发生在调用线程在调用__threadfence()之前对所有内存进行的任何写入之前。
系统级内存栅栏
cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_system);
确保:
调用线程在调用
cuda::atomic_thread_fence()之前对所有内存进行的所有写入,都会被设备中的所有线程、主机线程以及对等设备中的所有线程观察为发生在该调用线程在调用cuda::atomic_thread_fence()之后对所有内存进行的所有写入之前。
void __threadfence_system();
确保:
调用线程在调用
__threadfence_system()之前对所有内存进行的全部写入,在设备中的所有线程、主机线程以及对等设备中的所有线程看来,都发生在该调用线程调用__threadfence_system()之后对所有内存进行的全部写入之前。
在前面的代码示例中,我们可以按如下方式在代码中插入内存栅栏:
#include <cuda/atomic>
__device__ int X = 1, Y = 2;
__device__ void writeXY() {
X = 10;
cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_device);
Y = 20;
}
__device__ void readXY() {
int B = Y;
cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_device);
int A = 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,即readXY()在writeXY()之前执行,A等于 10 且B等于 20,即writeXY()在readXY()之前执行。A等于 10 且B等于 2。A为 1 且B为 20 的情况不可能出现,因为内存栅栏确保对X的写入先于对Y的写入可见。
如果线程 1 和线程 2 属于同一个线程块,则使用块级栅栏就足够了。如果线程 1 和线程 2 不属于同一个线程块,那么如果它们是同一设备上的 CUDA 线程,就必须使用设备级栅栏;如果它们是两个不同设备上的 CUDA 线程,就必须使用系统级栅栏。
下面的代码示例展示了一个常见用例,其中线程使用其他线程产生的数据。此核函数在一次调用中计算包含 N 个数的数组的总和。
每个线程块首先对数组的一个子集求和,并将结果存储到全局内存中。
当所有线程块都完成后,最后一个线程块从全局内存中读取每个部分和,并将它们相加以获得最终结果。
为了确定哪个线程块最后完成,每个线程块都以原子方式递增一个计数器,以指示它已完成部分和的计算和存储(有关更多详细信息,请参阅原子函数一节)。最后一个线程块获得的计数器值等于
gridDim.x - 1。
如果在存储部分和与递增计数器之间没有栅栏,计数器可能会在部分和存储之前递增。这可能导致计数器达到gridDim.x - 1,并使最后一个线程块在部分和在内存中更新之前就开始读取它们。
注意
内存栅栏只影响内存操作的执行顺序;它不保证这些操作对其他线程的可见性。
在下面的代码示例中,通过将result变量声明为volatile来确保对该变量的内存操作的可见性。有关更多详细信息,请参阅volatile限定的变量一节。
#include <cuda/atomic>
__device__ int count = 0;
__global__ void sum(const float* array,
int N,
volatile float* result) {
__shared__ bool isLastBlockDone;
// 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 increment of the "count" variable is
// only performed after the partial sum has been written to global memory.
cuda::atomic_thread_fence(cuda::memory_order_seq_cst, cuda::thread_scope_device);
// Thread 0 signals that it is done.
int count_old = atomicInc(&count, gridDim.x);
// Thread 0 determines if its block is the last block to be done.
isLastBlockDone = (count_old == (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;
}
}
}
5.4.5. 原子函数
原子函数对共享数据执行读-修改-写操作,使这些操作看起来像是在单个步骤中执行的。原子性确保每个操作要么完全完成,要么完全不执行,从而为所有参与线程提供一致的数据视图。
CUDA 以五种方式提供原子函数:
- 扩展 CUDA C++ 原子函数,cuda::atomic和cuda::atomic_ref。
它们允许在主机代码和设备代码中使用。
它们遵循C++ 标准原子操作的语义。
它们允许指定原子操作的线程作用域。
- 标准 C++ 原子函数,cuda::std::atomic和cuda::std::atomic_ref。
它们允许在主机代码和设备代码中使用。
它们遵循C++ 标准原子操作的语义。
它们不允许指定原子操作的线程作用域。
- 编译器内建原子函数,
__nv_atomic_<op>()。 它们自 CUDA 12.8 起可用。
它们只允许在设备代码中使用。
它们遵循C++ 标准原子内存序的语义。
它们允许指定原子操作的线程作用域。
它们具有与C++ 标准原子操作相同的内存排序语义。
它们支持cuda::std::atomic和cuda::std::atomic_ref所允许的数据类型的一个子集,但 128 位数据类型除外。
分块代码不支持它们。
- CUDA Tile C++ 原子函数(例如,
cuda::tiles::atomic_load): 它们只允许在分块代码中使用。
它们遵循C++ 标准原子内存序的语义。
它们允许通过
cuda::tiles::thread_scope指定线程作用域。
- 旧式原子函数,
atomic<Op>()。 它们只允许在设备代码中使用。
它们仅支持
memory_order_relaxedC++ 原子内存语义。它们允许将原子操作的线程作用域作为函数名的一部分来指定。
与内建原子函数不同,旧式原子函数只确保原子性,并不引入同步点(栅栏)。
它们支持内建原子函数所允许的数据类型的一个子集。原子
add操作支持额外的数据类型。
提示
出于效率、安全性和可移植性的考虑,建议使用libcu++提供的扩展 CUDA C++ 原子函数。
5.4.5.1. 旧式原子函数
旧式原子函数对存储在全局内存或共享内存中的 32 位、64 位或 128 位字执行原子读-修改-写操作。例如,atomicAdd()函数读取全局内存或共享内存中特定地址处的一个字,将一个数加到该字上,然后将结果写回同一地址。
原子函数只能在设备函数中使用。
对于
__half2、__nv_bfloat162、float2和float4等向量类型,读-修改-写操作针对向量的每个元素执行。不保证对整个向量的单次访问具有原子性。
本节所述原子函数的内存序为cuda::std::memory_order_relaxed,并且仅在特定的线程作用域内具有原子性:
不带后缀的原子 API,例如
atomicAdd,在cuda::thread_scope_device作用域内具有原子性。带有
_block后缀的原子 API,例如atomicAdd_block,在cuda::thread_scope_block作用域内具有原子性。带有
_system后缀的原子 API,例如atomicAdd_system,如果满足特定条件,则在cuda::thread_scope_system作用域内具有原子性。
以下示例展示了 CPU 和 GPU 以原子方式更新地址addr处的整数值:
#include <cuda_runtime.h>
__global__ void atomicAdd_kernel(int* addr) {
atomicAdd_system(addr, 10);
}
void test_atomicAdd(int device_id) {
int* addr;
cudaMallocManaged(&addr, 4);
*addr = 0;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, device_id);
if (deviceProp.concurrentManagedAccess != 1) {
return; // the device does not coherently access managed memory concurrently with the CPU
}
atomicAdd_kernel<<<...>>>(addr);
__sync_fetch_and_add(addr, 10); // CPU atomic operation
}
请注意,任何原子操作都可以基于atomicCAS()(比较并交换)来实现。例如,针对单精度浮点数的atomicAdd()可以按如下方式实现:
#include <cuda/memory>
#include <cuda/std/bit>
__device__ float customAtomicAdd(float* d_ptr, float value) {
volatile unsigned* d_ptr_unsigned = reinterpret_cast<unsigned*>(d_ptr);
unsigned old_value = *d_ptr_unsigned;
unsigned assumed;
do {
assumed = old_value;
float assumed_float = cuda::std::bit_cast<float>(assumed);
float expected_value = assumed_float + value;
unsigned expected_value_unsigned = cuda::std::bit_cast<unsigned>(expected_value);
old_value = atomicCAS(d_ptr_unsigned, assumed, expected_value_unsigned);
// Note: uses integer comparison to avoid hang in case of NaN (since NaN != NaN)
} while (assumed != old_value);
return cuda::std::bit_cast<float>(old_value);
}
请参阅Compiler Explorer上的示例。
5.4.5.1.1. atomicAdd()
T atomicAdd(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old + val。将结果存回内存中的同一地址。
该函数返回old值。
atomicAdd()支持以下数据类型:
int,unsigned,unsigned long long,float,double,__half2,__half.计算能力为 8.x 及更高的设备上的
__nv_bfloat16、__nv_bfloat162。计算能力为 9.x 及更高的设备上的
float2、float4,且仅支持全局内存地址。
当atomicAdd()应用于向量类型(例如__half2或float4)时,其原子性是针对每个分量分别保证的;不保证对整个向量的单次访问具有原子性。
5.4.5.1.2. atomicSub()
T atomicSub(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old - val。将结果存回内存中的同一地址。
该函数返回old值。
atomicSub()支持以下数据类型:
int,unsigned
5.4.5.1.3. atomicInc()
unsigned atomicInc(unsigned* address, unsigned val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old >= val ? 0 : (old + 1)。将结果存回内存中的同一地址。
该函数返回old值。
5.4.5.1.4. atomicDec()
unsigned atomicDec(unsigned* address, unsigned val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
(old == 0 || old > val) ? val : (old - 1)。将结果存回内存中的同一地址。
该函数返回old值。
5.4.5.1.5. atomicAnd()
T atomicAnd(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old & val。将结果存回内存中的同一地址。
该函数返回old值。
atomicAnd()支持以下数据类型:
int,unsigned,unsigned long long.
5.4.5.1.6. atomicOr()
T atomicOr(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old | val。将结果存回内存中的同一地址。
该函数返回old值。
atomicOr()支持以下数据类型:
int,unsigned,unsigned long long.
5.4.5.1.7. atomicXor()
T atomicXor(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old ^ val。将结果存回内存中的同一地址。
该函数返回old值。
atomicXor()支持以下数据类型:
int,unsigned,unsigned long long.
5.4.5.1.8. atomicMin()
T atomicMin(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old和val中的最小值。将结果存回内存中的同一地址。
该函数返回old值。
atomicMin()支持以下数据类型:
int,unsigned,unsigned long long,long long.
5.4.5.1.9. atomicMax()
T atomicMax(T* address, T val);
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old和val中的最大值。将结果存回内存中的同一地址。
该函数返回old值。
atomicMax()支持以下数据类型:
int,unsigned,unsigned long long,long long.
5.4.5.1.10. atomicExch()
T atomicExch(T* address, T val);
template<typename T>
T atomicExch(T* address, T val); // only 128-bit types, compute capability 9.x and higher
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。将
val存回内存中的同一地址。
该函数返回old值。
atomicExch()支持以下数据类型:
int,unsigned,unsigned long long,float.
C++ 模板函数atomicExch()支持 128 位类型,但须满足以下要求:
计算能力为 9.x 及更高。
T必须按 16 字节对齐,即alignof(T) >= 16。T必须是可平凡复制的,即std::is_trivially_copyable_v<T>。对于 C++03 及更早版本:
T必须是可平凡构造的,即std::is_default_constructible_v<T>。
5.4.5.1.11. atomicCAS()
T atomicCAS(T* address, T compare, T val);
template<typename T>
T atomicCAS(T* address, T compare, T val); // only 128-bit types, compute capability 9.x and higher
该函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old == compare ? val : old。将结果存回内存中的同一地址。
该函数返回old值。
atomicCAS()支持以下数据类型:
int,unsigned,unsigned long long,unsigned short.
C++ 模板函数atomicCAS()支持 128 位类型,但须满足以下要求:
计算能力为 9.x 及更高。
T必须按 16 字节对齐,即alignof(T) >= 16。T必须是可平凡复制的,即std::is_trivially_copyable_v<T>。对于 C++03 及更早版本:
T必须是可平凡构造的,即std::is_default_constructible_v<T>。
5.4.5.2. 内建原子函数
CUDA 12.8 及更高版本支持用于原子操作的 CUDA 编译器内建函数,它们遵循与C++ 标准原子操作相同的内存排序语义以及 CUDA 的线程作用域。这些函数遵循GNU 的原子内建函数签名,并增加了一个线程作用域参数。
当支持内建原子函数时,nvcc会定义宏__CUDACC_DEVICE_ATOMIC_BUILTINS__。
下文列出了内存序和线程作用域的原始枚举项,它们用作内建原子函数的order和scope参数:
// atomic memory orders
enum {
__NV_ATOMIC_RELAXED,
__NV_ATOMIC_CONSUME,
__NV_ATOMIC_ACQUIRE,
__NV_ATOMIC_RELEASE,
__NV_ATOMIC_ACQ_REL,
__NV_ATOMIC_SEQ_CST
};
// thread scopes
enum {
__NV_THREAD_SCOPE_THREAD,
__NV_THREAD_SCOPE_BLOCK,
__NV_THREAD_SCOPE_CLUSTER,
__NV_THREAD_SCOPE_DEVICE,
__NV_THREAD_SCOPE_SYSTEM
};
内存序对应于C++ 标准原子操作的内存序。
线程作用域遵循
cuda::thread_scope的定义。__NV_ATOMIC_CONSUME内存序目前使用更强的__NV_ATOMIC_ACQUIRE内存序来实现。__NV_THREAD_SCOPE_THREAD线程作用域目前使用更广的__NV_THREAD_SCOPE_BLOCK线程作用域来实现。
示例:
__device__ T __nv_atomic_load_n(T* pointer,
int memory_order,
int thread_scope = __NV_THREAD_SCOPE_SYSTEM);
原子内建函数具有以下限制:
它们只能在设备函数中使用。
它们不能对局部内存进行操作。
不能获取这些函数的地址。
order和scope参数必须是整数字面量;它们不能是变量。sm_90及更高的架构支持线程作用域__NV_THREAD_SCOPE_CLUSTER。
不支持的情况的示例:
// Not permitted in a host function
__host__ void bar() {
unsigned u1 = 1, u2 = 2;
__nv_atomic_load(&u1, &u2, __NV_ATOMIC_RELAXED, __NV_THREAD_SCOPE_SYSTEM);
}
// Not permitted to be applied to local memory
__device__ void foo() {
unsigned a = 1, b;
__nv_atomic_load(&a, &b, __NV_ATOMIC_RELAXED, __NV_THREAD_SCOPE_SYSTEM);
}
// Not permitted as a template default argument.
// The function address cannot be taken.
template<void *F = __nv_atomic_load_n>
class X {
void *f = F; // The function address cannot be taken.
};
// 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)) {}
};
5.4.5.2.1. __nv_atomic_fetch_add(), __nv_atomic_add()
__device__ T __nv_atomic_fetch_add(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_add (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old + val。将结果存回内存中的同一地址。
__nv_atomic_fetch_add返回old值。__nv_atomic_add没有返回值。
这些函数支持以下数据类型:
int,unsigned,unsigned long long,float,double.
5.4.5.2.2. __nv_atomic_fetch_sub(), __nv_atomic_sub()
__device__ T __nv_atomic_fetch_sub(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_sub (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old - val。将结果存回内存中的同一地址。
__nv_atomic_fetch_sub返回old值。__nv_atomic_sub没有返回值。
这些函数支持以下数据类型:
int,unsigned,unsigned long long,float,double.
5.4.5.2.3. __nv_atomic_fetch_and(), __nv_atomic_and()
__device__ T __nv_atomic_fetch_and(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_and (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old & val。将结果存回内存中的同一地址。
__nv_atomic_fetch_and返回old值。__nv_atomic_and没有返回值。
这些函数支持以下数据类型:
大小为 4 或 8 字节的任何整数类型。
5.4.5.2.4. __nv_atomic_fetch_or(), __nv_atomic_or()
__device__ T __nv_atomic_fetch_or(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_or (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old | val。将结果存回内存中的同一地址。
__nv_atomic_fetch_or返回old值。__nv_atomic_or没有返回值。
这些函数支持以下数据类型:
大小为 4 或 8 字节的任何整数类型。
5.4.5.2.5. __nv_atomic_fetch_xor(), __nv_atomic_xor()
__device__ T __nv_atomic_fetch_xor(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_xor (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old ^ val。将结果存回内存中的同一地址。
__nv_atomic_fetch_xor返回old值。__nv_atomic_xor没有返回值。
这些函数支持以下数据类型:
大小为 4 或 8 字节的任何整数类型。
5.4.5.2.6. __nv_atomic_fetch_min(), __nv_atomic_min()
__device__ T __nv_atomic_fetch_min(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_min (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old和val中的最小值。将结果存回内存中的同一地址。
__nv_atomic_fetch_min返回old值。__nv_atomic_min没有返回值。
这些函数支持以下数据类型:
unsigned,int,unsigned long long,long long.
5.4.5.2.7. __nv_atomic_fetch_max(), __nv_atomic_max()
__device__ T __nv_atomic_fetch_max(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_max (T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。计算
old和val中的最大值。将结果存回内存中的同一地址。
__nv_atomic_fetch_max返回old值。__nv_atomic_max没有返回值。
这些函数支持以下数据类型:
unsigned,int,unsigned long long,long long
5.4.5.2.8. __nv_atomic_exchange(), __nv_atomic_exchange_n()
__device__ T __nv_atomic_exchange_n(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_exchange (T* address, T* val, T* ret, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一个原子事务中执行以下操作:
读取全局内存或共享内存中地址
address处的old值。__nv_atomic_exchange_n将val存储到address所指向的位置。__nv_atomic_exchange将old存储到ret所指向的位置,并将地址val处的值存储到address所指向的位置。
__nv_atomic_exchange_n返回old值。__nv_atomic_exchange没有返回值。
这些函数支持以下数据类型:
大小为 4、8 或 16 字节的任何数据类型。
计算能力为 9.x 及更高的设备支持 16 字节数据类型。
5.4.5.2.9. __nv_atomic_compare_exchange(), __nv_atomic_compare_exchange_n()
__device__ bool __nv_atomic_compare_exchange (T* address, T* expected, T* desired, bool weak, int success_order, int failure_order,
int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ bool __nv_atomic_compare_exchange_n(T* address, T* expected, T desired, bool weak, int success_order, int failure_order,
int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一次原子事务中执行以下操作:
读取位于全局内存或共享内存中地址
address处的old值。将
old与expected所指向位置的值进行比较。如果两者相等,则返回值为
true,并将desired存储到address所指向的位置。否则,返回false,并将old存储到expected所指向的位置。
参数weak被忽略,函数会在success_order和failure_order之间选择较强的内存序来执行比较并交换操作。
这些函数支持以下数据类型:
大小为 2、4、8 或 16 字节的任意数据类型。
计算能力为 9.x 及更高的设备支持 16 字节数据类型。
5.4.5.2.10. __nv_atomic_load(), __nv_atomic_load_n()
__device__ void __nv_atomic_load (T* address, T* ret, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ T __nv_atomic_load_n(T* address, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一次原子事务中执行以下操作:
读取位于全局内存或共享内存中地址
address处的old值。__nv_atomic_load将old存储到ret所指向的位置。__nv_atomic_load_n返回old。
这些函数支持以下数据类型:
大小为 1、2、4、8 或 16 字节的任意数据类型。
order不能为__NV_ATOMIC_RELEASE或__NV_ATOMIC_ACQ_REL。
5.4.5.2.11. __nv_atomic_store(), __nv_atomic_store_n()
__device__ void __nv_atomic_store (T* address, T* val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
__device__ void __nv_atomic_store_n(T* address, T val, int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
这些函数在一次原子事务中执行以下操作:
读取位于全局内存或共享内存中地址
address处的old值。__nv_atomic_store读取val所指向位置的值,并将其存储到address所指向的位置。__nv_atomic_store_n将val存储到address所指向的位置。
order不能为__NV_ATOMIC_CONSUME、__NV_ATOMIC_ACQUIRE或__NV_ATOMIC_ACQ_REL。
5.4.5.2.12. __nv_atomic_thread_fence()
__device__ void __nv_atomic_thread_fence(int order, int scope = __NV_THREAD_SCOPE_SYSTEM);
此原子函数根据指定的内存序,在此线程请求的内存访问之间建立先后顺序。线程作用域参数指定可能观察到此操作排序效果的线程集合。
5.4.6. 线程束函数
以下小节介绍线程束函数,这些函数允许线程束内的线程相互通信并执行计算。
提示
出于效率、安全性和可移植性的考虑,建议尽可能使用CUB的线程束范围的“集体”原语来执行线程束操作。
5.4.6.1. 线程束活跃掩码
unsigned __activemask();
该函数返回一个 32 位整数掩码,表示调用线程束中当前所有活跃线程。如果在线程束中调用__activemask()时第 N 个通道处于活跃状态,则第 N 位被置位。非活跃线程在返回的掩码中由值为 0 的位表示。已退出程序的线程始终被标记为非活跃。
警告
__activemask()不能用于确定哪些线程束通道执行给定的分支。此函数旨在用于机会式线程束级编程,并且仅提供线程束内活跃线程的瞬时快照。
// Check whether at least one thread's predicate evaluates to true
if (pred) {
// Invalid: the value of 'at_least_one' is non-deterministic
// and could vary between executions.
at_least_one = __activemask() > 0;
}
请注意,在__activemask()调用处收敛的线程不保证在后续指令处仍保持收敛,除非这些指令是线程束同步内建函数(__sync)。
例如,编译器可能对指令进行重排,而活跃线程集合可能不会保持不变:
unsigned mask = __activemask(); // Assume mask == 0xFFFFFFFF (all bits set, all threads active)
int predicate = threadIdx.x % 2 == 0; // 1 for even threads, 0 for odd threads
int result = __any_sync(mask, predicate); // Active threads might not be preserved
5.4.6.2. 线程束投票函数
int __all_sync (unsigned mask, int predicate);
int __any_sync (unsigned mask, int predicate);
unsigned __ballot_sync(unsigned mask, int predicate);
线程束投票函数使给定线程束中的线程能够执行归约并广播操作。这些函数从线程束中每个尚未退出的线程获取一个整数predicate作为输入,并将这些值与零进行比较。随后,以以下方式之一在线程束的活跃线程之间组合(归约)比较结果,并将单个返回值广播给每个参与线程:
__all_sync(unsigned mask, predicate):对
mask中所有尚未退出的线程求predicate的值,如果对所有这些线程求得的predicate值都非零,则返回非零值。__any_sync(unsigned mask, predicate):对
mask中所有尚未退出的线程求predicate的值,如果对其中一个或多个线程求得的predicate值非零,则返回非零值。__ballot_sync(unsigned mask, predicate):对
mask中所有尚未退出的线程求predicate的值,并返回一个整数;如果对线程束中第 N 个线程求得的predicate值非零,且第 N 个线程处于活跃状态,则该整数的第 N 位被置位。否则,第 N 位为零。
这些函数受线程束 __sync 内建函数约束的限制。
警告
这些内建函数不提供任何内存顺序保证。
5.4.6.3. 线程束匹配函数
提示
建议使用libcu++的cuda::device::warp_match_all()函数作为__match_all_sync函数的更通用、更安全的替代方案。
unsigned __match_any_sync(unsigned mask, T value);
unsigned __match_all_sync(unsigned mask, T value, int *pred);
线程束匹配函数在线程束内尚未退出的线程之间对一个变量执行广播并比较操作。
__match_any_sync返回
mask中具有按位相同的value的尚未退出线程的掩码。__match_all_sync如果
mask中所有尚未退出的线程都具有按位相同的value,则返回mask;否则返回 0。如果mask中所有尚未退出的线程都具有按位相同的value,则将谓词pred设置为true;否则将该谓词设置为假。
T可以为int、unsigned、long、unsigned long、long long、unsigned long long、float或double。
这些函数受线程束 __sync 内建函数约束的限制。
警告
这些内建函数不提供任何内存顺序保证。
5.4.6.4. 线程束归约函数
提示
出于效率、安全性和可移植性的考虑,建议尽可能使用CUB的线程束范围的“集体”原语来执行线程束归约。
计算能力为 8.x 或更高的设备支持这些函数。
T __reduce_add_sync(unsigned mask, T value);
T __reduce_min_sync(unsigned mask, T value);
T __reduce_max_sync(unsigned mask, T value);
unsigned __reduce_and_sync(unsigned mask, unsigned value);
unsigned __reduce_or_sync (unsigned mask, unsigned value);
unsigned __reduce_xor_sync(unsigned mask, unsigned value);
__reduce_<op>_sync内建函数在同步mask中指定的所有尚未退出的线程之后,对value中提供的数据执行归约操作。
__reduce_add_sync,__reduce_min_sync,__reduce_max_sync对
mask中指定的每个尚未退出的线程在value中提供的值执行算术加法、最小值或最大值归约操作,并返回其结果。T可以是unsigned或signed整数。__reduce_and_sync,__reduce_or_sync,__reduce_xor_sync对
mask中指定的每个尚未退出的线程在value中提供的值执行按位与、按位或或按位异或归约操作,并返回其结果。
这些函数受线程束 __sync 内建函数约束的限制。
警告
这些内建函数不提供任何内存顺序保证。
5.4.6.5. 线程束洗牌函数
提示
建议使用libcu++的cuda::device::warp_shuffle()函数作为__shfl_sync()和__shfl_<op>_sync()内建函数的更通用、更安全的替代方案。
T __shfl_sync (unsigned mask, T value, int srcLane, int width=warpSize);
T __shfl_up_sync (unsigned mask, T value, unsigned delta, int width=warpSize);
T __shfl_down_sync(unsigned mask, T value, unsigned delta, int width=warpSize);
T __shfl_xor_sync (unsigned mask, T value, int laneMask, int width=warpSize);
线程束洗牌函数在线程束内尚未退出的线程之间交换一个值,而不使用共享内存。
__shfl_sync():从索引指定的通道直接复制。该内建函数返回 ID 由
srcLane指定的线程所持有的value值。如果
width小于warpSize,则线程束的每个子区段都作为一个独立实体运行,其起始逻辑通道 ID 为 0。如果
srcLane超出范围[0, width - 1],则结果对应于同一子区段内的srcLane % width所持有的值。
__shfl_up_sync():从 ID 小于调用者的通道复制。该内建函数通过从调用者的通道 ID 中减去
delta来计算源通道 ID。返回计算得到的通道 ID 所持有的value值:其效果是将value在线程束内向上移动delta个通道。如果
width小于warpSize,则线程束的每个子区段都作为一个独立实体运行,其起始逻辑通道 ID 为 0。源通道索引不会以
width的值为界回绕,因此较低的delta个通道将保持不变。
__shfl_down_sync():从 ID 大于调用者的通道复制。该内建函数通过将
delta加到调用者的通道 ID 上来计算源通道 ID。返回计算得到的通道 ID 所持有的value值:其效果是将value在线程束内向下移动delta个通道。如果
width小于warpSize,则线程束的每个子区段都作为一个独立实体运行,其起始逻辑通道 ID 为 0。与
__shfl_up_sync()一样,源通道的 ID 编号不会以 width 的值为界回绕,因此较高的delta个通道实际上将保持不变。
__shfl_xor_sync():根据自身通道 ID 的按位异或结果,从某个通道复制。该内建函数通过对调用者的通道 ID 与
laneMask执行按位异或来计算源通道 ID:返回计算得到的通道 ID 所持有的value值。此模式实现了一种蝶形寻址模式,用于树形归约和广播。如果
width小于warpSize,则每组width个连续线程都能够访问前面各组中的元素。但是,如果它们尝试访问后面各组线程中的元素,则会返回它们自身的value值。
T可以为:
int、unsigned、long、unsigned long、long long、unsigned long long、float或double。包含
cuda_fp16.h头文件时,可以为__half和__half2。包含
cuda_bf16.h头文件时,可以为__nv_bfloat16和__nv_bfloat162。
线程只能从正在活跃参与这些内建函数的另一个线程读取数据。如果目标线程处于非活跃状态,则获取的值未定义。
width必须是范围[1, warpSize]内的 2 的幂,即 1、2、4、8、16 或 32。其他值将产生未定义的结果。
这些函数受线程束 __sync 内建函数约束的限制。
有效的线程束洗牌用法示例:
int laneId = threadIdx.x % warpSize;
int data = ...
// all warp threads get 'data' from lane 0
int result1 = __shfl_sync(0xFFFFFFFF, data, 0);
if (laneId < 4) {
// lanes 0, 1, 2, 3 get 'data' from lane 1
int result2 = __shfl_sync(0xb1111, data, 1);
}
// lanes [0 - 15] get 'data' from lane 0
// lanes [16 - 31] get 'data' from lane 16
int result3 = __shfl_sync(0xFFFFFFFF, value, warpSize / 2);
// each lane gets 'data' from the lane two positions above
// lanes 30, 31 get their original value
int result4 = __shfl_down_sync(0xFFFFFFFF, data, 2);
无效的线程束洗牌用法示例:
int laneId = threadIdx.x % warpSize;
int value = ...
// undefined behavior: lane 0 does not participate in the call
int result = (laneId > 0) ? __shfl_sync(0xFFFFFFFF, value, 0) : 0;
if (laneId <= 4) {
// undefined behavior: destination lanes 5, 6 are not active for lanes 3, 4
result = __shfl_down_sync(0b11111, value, 2);
}
// undefined behavior: width is not a power of 2
__shfl_sync(0xFFFFFFFF, value, 0, /*width=*/31);
警告
这些内建函数并不隐含内存屏障。它们不保证任何内存顺序。
示例 1:在线程束内广播单个值
#include <cassert>
#include <cuda/warp>
__global__ void warp_broadcast_kernel(int input) {
int laneId = threadIdx.x % 32;
int value;
if (laneId == 0) { // unused variable for all threads except lane 0
value = input;
}
value = cuda::device::warp_shuffle_idx(value, 0); // Synchronize all threads in warp, and get "value" from lane 0
assert(value == input);
}
int main() {
warp_broadcast_kernel<<<1, 32>>>(1234);
cudaDeviceSynchronize();
return 0;
}
#include <assert.h>
__global__ void warp_broadcast_kernel(int input) {
int laneId = threadIdx.x % 32;
int value;
if (laneId == 0) { // unused variable for all threads except lane 0
value = input;
}
value = __shfl_sync(0xFFFFFFFF, value, 0); // Synchronize all threads in warp, and get "value" from lane 0
assert(value == input);
}
int main() {
warp_broadcast_kernel<<<1, 32>>>(1234);
cudaDeviceSynchronize();
return 0;
}
请参见Compiler Explorer上的示例。
示例 2:在由 8 个线程组成的子分区内执行包含式加法扫描
提示
建议使用cub::WarpScan函数来实现高效且通用的线程束扫描功能。
#include <cstdio>
#include <cub/cub.cuh>
__global__ void scan_sub_partition_with_8_threads_kernel() {
using WarpScan = cub::WarpScan<int, 8>;
using TempStorage = typename WarpScan::TempStorage;
__shared__ TempStorage temp_storage;
int laneId = threadIdx.x % 32;
int value = 31 - laneId; // starting value to accumulate
int partial_sum;
WarpScan(temp_storage).InclusiveSum(value, partial_sum);
printf("Thread %d final value = %d\n", threadIdx.x, partial_sum);
}
int main() {
scan_sub_partition_with_8_threads_kernel<<<1, 32>>>();
cudaDeviceSynchronize();
return 0;
}
#include <stdio.h>
__global__ void scan_sub_partition_with_8_threads_kernel() {
int laneId = threadIdx.x % 32;
int value = 31 - laneId; // starting value to accumulate
// Loop to accumulate scan within my partition.
// Scan requires log2(8) == 3 steps for 8 threads
for (int delta = 1; delta <= 4; delta *= 2) {
int tmp = __shfl_up_sync(0xFFFFFFFF, value, delta, /*width=*/8); // read from laneId - delta
int source_lane = laneId % 8 - delta;
if (source_lane >= 0) // lanes with 'source_lane < 0' have their value unchanged
value += tmp;
}
printf("Thread %d final value = %d\n", threadIdx.x, value);
}
int main() {
scan_sub_partition_with_8_threads_kernel<<<1, 32>>>();
cudaDeviceSynchronize();
return 0;
}
请参见Compiler Explorer上的示例。
示例 3:线程束内归约
提示
建议使用cub::WarpReduce函数来实现高效且通用的线程束归约功能。
#include <cstdio>
#include <cub/cub.cuh>
#include <cuda/warp>
__global__ void warp_reduce_kernel() {
using WarpReduce = cub::WarpReduce<int>;
using TempStorage = typename WarpReduce::TempStorage;
__shared__ TempStorage temp_storage;
int laneId = threadIdx.x % 32;
int value = 31 - laneId; // starting value to accumulate
auto aggregate = WarpReduce(temp_storage).Sum(value);
aggregate = cuda::device::warp_shuffle_idx(aggregate, 0);
printf("Thread %d final value = %d\n", threadIdx.x, aggregate);
}
int main() {
warp_reduce_kernel<<<1, 32>>>();
cudaDeviceSynchronize();
return 0;
}
#include <stdio.h>
__global__ void warp_reduce_kernel() {
int laneId = threadIdx.x % 32;
int value = 31 - laneId; // starting value to accumulate
// Use XOR mode to perform butterfly reduction
// A full-warp reduction requires log2(32) == 5 steps
for (int i = 1; i <= 16; i *= 2)
value += __shfl_xor_sync(0xFFFFFFFF, value, i);
// "value" now contains the sum across all threads
printf("Thread %d final value = %d\n", threadIdx.x, value);
}
int main() {
warp_reduce_kernel<<<1, 32>>>();
cudaDeviceSynchronize();
return 0;
}
请参见Compiler Explorer上的示例。
5.4.6.6. 线程束__sync内建函数约束
所有线程束__sync内建函数,例如:
__shfl_sync,__shfl_up_sync,__shfl_down_sync,__shfl_xor_sync__match_any_sync,__match_all_sync__reduce_add_sync,__reduce_min_sync,__reduce_max_sync,__reduce_and_sync,__reduce_or_sync,__reduce_xor_sync__syncwarp
都使用mask参数来指示哪些线程束线程参与调用。此参数确保在硬件执行内建函数之前正确收敛。
mask中的每一位都对应于一个线程的通道 ID(threadIdx.x % warpSize)。该内建函数会等待,直到mask中指定的所有尚未退出的线程束线程都到达调用处。每个调用线程在
mask中的对应位都必须被置位。每个未调用线程在
mask中的对应位都必须设置为零。已退出的线程将被忽略。mask中指定的所有尚未退出的线程都必须使用相同的mask值执行该内建函数。线程束线程可以使用不同的
mask值并发调用该内建函数,前提是这些掩码互不相交。即使在分歧控制流中,这种情况也是有效的。
如果出现以下情况,线程束__sync函数的行为将无效(例如核函数挂起)或未定义:
某个调用线程未在
mask中指定。mask中指定的某个尚未退出的线程既未最终退出,也未在相同程序位置以相同的mask值调用该内建函数。在条件代码中,对于
mask中指定的所有尚未退出的线程,所有条件的求值结果都必须相同。
注意
当所有线程束线程都参与调用时,即当mask设置为0xFFFFFFFF时,这些内建函数可达到最佳效率。
有效的线程束内建函数用法示例:
__global__ void valid_examples() {
if (threadIdx.x < 4) { // threads 0, 1, 2, 3 are active
__all_sync(0b1111, pred); // CORRECT, threads 0, 1, 2, 3 participate in the call
}
if (threadIdx.x == 0)
return; // exit
// CORRECT, all non-exited threads participate in the call
__all_sync(0xFFFFFFFF, pred);
}
互不相交的mask示例:
__global__ void example_syncwarp_with_mask(int* input_data, int* output_data) {
if (threadIdx.x < warpSize) {
__shared__ int shared_data[warpSize];
shared_data[threadIdx.x] = input_data[threadIdx.x];
unsigned mask = threadIdx.x < 16 ? 0xFFFF : 0xFFFF0000; // CORRECT
__syncwarp(mask);
if (threadIdx.x == 0 || threadIdx.x == 16)
output_data[threadIdx.x] = shared_data[threadIdx.x + 1];
}
}
__global__ void example_syncwarp_with_mask_branches(int* input_data, int* output_data) {
if (threadIdx.x < warpSize) {
__shared__ int shared_data[warpSize];
shared_data[threadIdx.x] = input_data[threadIdx.x];
if (threadIdx.x < 16) {
unsigned mask = 0xFFFF; // CORRECT
__syncwarp(mask);
output_data[threadIdx.x] = shared_data[15 - threadIdx.x];
}
else {
unsigned mask = 0xFFFF0000; // CORRECT
__syncwarp(mask);
output_data[threadIdx.x] = shared_data[31 - threadIdx.x];
}
}
}
无效的线程束内建函数用法示例:
if (threadIdx.x < 4) { // threads 0, 1, 2, 3 are active
__all_sync(0b0000011, pred); // WRONG, threads 2, 3 are active but not set in mask
__all_sync(0b1111111, pred); // WRONG, threads 4, 5, 6 are not active but set in mask
}
// WRONG, participating threads have a different and overlapping mask
__all_sync(threadIdx.x == 0 ? 1 : 0xFFFFFFFF, pred);
5.4.7. CUDA 特定宏
5.4.7.1. __CUDA_ARCH__
宏__CUDA_ARCH__表示正在为其编译代码的 NVIDIA GPU 的虚拟架构。其值可能与设备的实际计算能力不同。此宏支持编写针对特定 GPU 架构的专用代码路径,这对于获得最佳性能或使用架构特定的特性和指令可能是必要的。此宏还可用于区分主机代码和设备代码。
__CUDA_ARCH__仅在设备代码中定义,即在__device__、__host__ __device__和__global__函数中定义。该宏的值与nvcc选项compute_<version>相关联,其关系为__CUDA_ARCH__ = <version> * 10。
示例:
nvcc --generate-code arch=compute_80,code=sm_90 prog.cu
将__CUDA_ARCH__定义为800。
__CUDA_ARCH__约束
1.以下实体的类型签名不得依赖于是否定义了__CUDA_ARCH__,也不得依赖于其值。
__global__函数和函数模板。__device__和__constant__变量。纹理和表面。
示例:
#if !defined(__CUDA_ARCH__)
typedef int my_type;
#else
typedef double my_type;
#endif
__device__ my_type my_var; // ERROR: my_var's type depends on __CUDA_ARCH__
__global__ void kernel(my_type in) { // ERROR: kernel's type depends on __CUDA_ARCH__
...
}
2.__global__函数模板的实例化不得依赖于是否定义了__CUDA_ARCH__或其值。也就是说,具有等价模板实参的相同实例化必须存在于所有设备程序和主机程序中(无论这些实例化是否会在运行时被启动)。
示例:
__device__ int result;
template <typename T>
__global__ void kernel(T in) {
result = in;
}
__host__ __device__ void host_device_function(void) {
#if !defined(__CUDA_ARCH__)
kernel<<<1, 1>>>(1); // ERROR: "kernel<int>" instantiation only
// when __CUDA_ARCH__ is undefined!
#endif
}
int main(void) {
host_device_function();
cudaDeviceSynchronize();
return 0;
}
可以通过将实际计算从__global__函数移到由前者调用的__device__函数模板中来避免此问题。这样,在__global__函数内,便可使用__CUDA_ARCH__有条件地以不同实参实例化__device__函数模板。
3.在分离编译模式下,具有外部链接的函数或变量定义是否存在,不得依赖于__CUDA_ARCH__的定义或其值。
示例:
#if !defined(__CUDA_ARCH__)
void host_function(void) {} // ERROR: The definition of host_function()
// is only present when __CUDA_ARCH__
// is undefined
#endif
4.在分离编译中,不得在头文件中使用预处理器宏__CUDA_ARCH__,以防止目标文件具有不同的行为。或者,所有目标文件都必须针对相同的虚拟架构进行编译。如果在头文件中定义了弱函数或模板函数,且其行为依赖于__CUDA_ARCH__,那么当不同目标文件针对不同的计算架构进行编译时,该函数在这些目标文件中的实例可能发生冲突。
例如,如果头文件a.h包含:
template<typename T>
__device__ T* get_ptr() {
#if __CUDA_ARCH__ == 900
return nullptr; /* no address */
#else
__shared__ T arr[256];
return arr;
#endif
}
那么,如果a.cu和b.cu都包含a.h并针对相同类型实例化get_ptr(),且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
Only one version of the ``get_ptr()`` function is used at link time, so the behavior depends on which version is chosen. To avoid this issue, either ``a.cu`` and ``b.cu`` must be compiled for the same compute architecture, or ``__CUDA_ARCH__`` should not be used in the shared header function.
对于上述不受支持的__CUDA_ARCH__用法,编译器不保证会生成诊断信息。
5.4.7.2. __CUDA_ARCH_SPECIFIC__和__CUDA_ARCH_FAMILY_SPECIFIC__
宏__CUDA_ARCH_SPECIFIC__和__CUDA_ARCH_FAMILY_SPECIFIC__分别被定义用于标识具有架构特定特性和系列特定特性的 GPU 设备。有关更多信息,请参见特性集编译器目标一节。
与__CUDA_ARCH__类似,__CUDA_ARCH_SPECIFIC__和__CUDA_ARCH_FAMILY_SPECIFIC__仅在设备代码中定义,即在__device__、__host__ __device__和__global__函数中定义。这些宏与nvcc选项compute_<version>a和compute_<version>f相关联。
nvcc --generate-code arch=compute_100a,code=sm_100a prog.cu
__CUDA_ARCH__ == 1000.__CUDA_ARCH_SPECIFIC__ == 1000.__CUDA_ARCH_FAMILY_SPECIFIC__ == 1000.
nvcc --generate-code arch=compute_100f,code=sm_103f prog.cu
__CUDA_ARCH__ == 1000.__CUDA_ARCH_FAMILY_SPECIFIC__ == 1000.__CUDA_ARCH_SPECIFIC__未定义。
nvcc -arch=sm_100 prog.cu
__CUDA_ARCH__ == 1000.__CUDA_ARCH_FAMILY_SPECIFIC__未定义。__CUDA_ARCH_SPECIFIC__未定义。
nvcc -arch=sm_100a prog.cu
# equivalent to:
nvcc --generate-code arch=sm_100a,compute_100,compute_100a prog.cu
__CUDA_ARCH__ == 1000.__CUDA_ARCH_FAMILY_SPECIFIC__未定义。__CUDA_ARCH_SPECIFIC__ == 1000和__CUDA_ARCH_SPECIFIC__未定义这两种情况都会生成。
5.4.7.3. CUDA 特性测试宏
nvcc提供以下用于特性测试的预处理器宏。当 CUDA 前端编译器支持某项特性时,就会定义相应的宏。
__CUDACC_DEVICE_ATOMIC_BUILTINS__:支持设备原子编译器内建函数。__NVCC_DIAG_PRAGMA_SUPPORT__:支持诊断控制编译指示。__CUDACC_EXTENDED_LAMBDA__:支持扩展 lambda。由--expt-extended-lambda或--extended-lambda标志启用。__CUDACC_RELAXED_CONSTEXPR__:支持宽松 constexpr 函数。由--expt-relaxed-constexpr标志启用。
5.4.7.4. __nv_pure__属性
在 C/C++ 中,纯函数对其参数没有副作用,并且可以访问全局变量,但不会修改它们。
CUDA 提供了__nv_pure__属性,主机函数和设备函数均支持该属性。编译器将__nv_pure__转换为 GNU 的pure属性或 Microsoft Visual Studio 的noalias属性。
__device__ __nv_pure__
int add(int a, int b) {
return a + b;
}
5.4.8. CUDA 专用函数
5.4.8.1. 地址空间谓词函数
地址空间谓词函数用于确定指针的地址空间。
提示
建议使用libcu++提供的cuda::device::is_address_from()和cuda::device::is_object_from()函数,作为地址空间谓词内建函数的可移植且更安全的替代方案。
__device__ unsigned __isGlobal (const void* ptr);
__device__ unsigned __isShared (const void* ptr);
__device__ unsigned __isConstant (const void* ptr);
__device__ unsigned __isGridConstant(const void* ptr);
__device__ unsigned __isLocal (const void* ptr);
如果ptr包含指定地址空间中某个对象的通用地址,这些函数返回1,否则返回0。如果实参是NULL指针,则其行为未指定。
__isGlobal():全局内存空间。__isShared():共享内存空间。__isConstant():常量内存空间。__isGridConstant():用__grid_constant__标注的核函数参数。__isLocal():局部内存空间。
5.4.8.2. 地址空间转换函数
CUDA 指针(T*)可以访问对象,而不受对象存储位置的限制。例如,无论int对象位于全局内存还是共享内存中,int*都可以访问这些对象。
地址空间转换函数用于在通用地址与特定地址空间中的地址之间进行转换。当编译器无法确定指针的地址空间时,例如跨越翻译单元或与 PTX 指令交互时,这些函数非常有用。
__device__ size_t __cvta_generic_to_global (const void* ptr); // PTX: cvta.to.global
__device__ size_t __cvta_generic_to_shared (const void* ptr); // PTX: cvta.to.shared
__device__ size_t __cvta_generic_to_constant(const void* ptr); // PTX: cvta.to.const
__device__ size_t __cvta_generic_to_local (const void* ptr); // PTX: cvta.to.local
__device__ void* __cvta_global_to_generic (size_t raw_ptr); // PTX: cvta.global
__device__ void* __cvta_shared_to_generic (size_t raw_ptr); // PTX: cvta.shared
__device__ void* __cvta_constant_to_generic(size_t raw_ptr); // PTX: cvta.const
__device__ void* __cvta_local_to_generic (size_t raw_ptr); // PTX: cvta.local
以与 PTX 指令互操作为例,ld.shared.s32 r0, [ptr]; PTX 指令要求ptr指向共享内存地址空间。对于具有指向__shared__内存中对象的int*指针的 CUDA 程序,在将该指针传递给 PTX 指令之前,需要调用__cvta_generic_to_shared将其转换到共享地址空间,如下所示:
__shared__ int smem_var;
smem_var = 42;
size_t smem_ptr = __cvta_generic_to_shared(&smem_var);
int output;
asm volatile("ld.shared.s32 %0, [%1];" : "=r"(output) : "l"(smem_ptr) : "memory");
assert(output == 42);
利用这些地址表示的一种常见优化是,利用共享空间、局部空间和常量空间的地址范围小于 32 位这一事实来减小数据结构的大小,这样就可以存储 32 位地址而不是 64 位指针,从而节省寄存器。此外,32 位算术运算比 64 位算术运算更快。要获得这些地址的 32 位整数表示,可通过将无符号 64 位整数强制转换为无符号 32 位整数,将 64 位值截断为 32 位:
__shared__ int smem_var;
uint32_t smem_ptr_32bit = static_cast<uint32_t>(__cvta_generic_to_shared(&smem_var));
要从这种 32 位表示恢复通用地址,请将该地址零扩展回无符号 64 位整数,然后调用相应的地址空间转换函数:
size_t smem_ptr_64bit = static_cast<size_t>(smem_ptr_32bit); // zero-extend to 64 bits
void* generic_ptr = __cvta_shared_to_generic(smem_ptr_64bit);
assert(generic_ptr == &smem_var);
5.4.8.3. 低级加载和存储函数
T __ldg(const T* address);
函数__ldg()执行只读 L1/Tex 缓存加载。它支持所有 C++ 基本类型、CUDA 向量类型(三分量类型除外)以及扩展浮点类型,例如__half、__half2、__nv_bfloat16和__nv_bfloat162。
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);
这些函数使用PTX ISA指南中指定的缓存运算符执行加载。它们支持所有 C++ 基本类型、CUDA 向量类型(三分量类型除外)以及扩展浮点类型,例如__half、__half2、__nv_bfloat16和__nv_bfloat162。
void __stwb(T* address, T value);
void __stcg(T* address, T value);
void __stcs(T* address, T value);
void __stwt(T* address, T value);
这些函数使用PTX ISA指南中指定的缓存运算符执行存储。它们支持所有 C++ 基本类型、CUDA 向量类型(三分量类型除外)以及扩展浮点类型,例如__half、__half2、__nv_bfloat16和__nv_bfloat162。
5.4.8.4. __trap()
可以从任意设备线程调用__trap()函数来发起陷阱操作。
void __trap();
核函数的执行会中止,并在主机程序中引发中断。调用__trap()会导致 CUDA 上下文损坏,使后续的 CUDA 调用和核函数调用失败。
5.4.8.5. __nanosleep()
__device__ void __nanosleep(unsigned nanoseconds);
函数__nanosleep(ns)将线程挂起,休眠时间约为ns纳秒。最长休眠时间约为一毫秒。
示例:
以下代码实现了一个采用指数退避的互斥锁。
__device__ void mutex_lock(unsigned* mutex) {
unsigned ns = 8;
while (atomicCAS(mutex, 0, 1) == 1) {
__nanosleep(ns);
if (ns < 256) {
ns *= 2;
}
}
}
__device__ void mutex_unlock(unsigned *mutex) {
atomicExch(mutex, 0);
}
5.4.8.6. 动态规划扩展(DPX)指令
DPX 函数组可以对最多三个 16 位或 32 位有符号或无符号整数参数求最小值和最大值,以及执行加法与求最小值/最大值的融合运算。此外,还提供可选的 ReLU 功能,即将值钳制到零。
比较函数:
三个参数。语义:
max(a, b, c)、min(a, b, c)。
int __vimax3_s32 ( int, int, int);
unsigned __vimax3_s16x2(unsigned, unsigned, unsigned);
unsigned __vimax3_u32 (unsigned, unsigned, unsigned);
unsigned __vimax3_u16x2(unsigned, unsigned, unsigned);
int __vimin3_s32 ( int, int, int);
unsigned __vimin3_s16x2(unsigned, unsigned, unsigned);
unsigned __vimin3_u32 (unsigned, unsigned, unsigned);
unsigned __vimin3_u16x2(unsigned, unsigned, unsigned);
两个参数,带 ReLU。语义:
max(a, b, 0)、max(min(a, b), 0)。
int __vimax_s32_relu ( int, int);
unsigned __vimax_s16x2_relu(unsigned, unsigned);
int __vimin_s32_relu ( int, int);
unsigned __vimin_s16x2_relu(unsigned, unsigned);
三个参数,带 ReLU。语义:
max(a, b, c, 0)、max(min(a, b, c), 0)。
int __vimax3_s32_relu ( int, int, int);
unsigned __vimax3_s16x2_relu(unsigned, unsigned, unsigned);
int __vimin3_s32_relu ( int, int, int);
unsigned __vimin3_s16x2_relu(unsigned, unsigned, unsigned);
两个参数,同时返回哪个参数较小/较大:
int __vibmax_s32 ( int, int, bool* pred);
unsigned __vibmax_u32 (unsigned, unsigned, bool* pred);
unsigned __vibmax_s16x2(unsigned, unsigned, bool* pred);
unsigned __vibmax_u16x2(unsigned, unsigned, bool* pred);
int __vibmin_s32 ( int, int, bool* pred);
unsigned __vibmin_u32 (unsigned, unsigned, bool* pred);
unsigned __vibmin_s16x2(unsigned, unsigned, bool* pred);
unsigned __vibmin_u16x2(unsigned, unsigned, bool* pred);
加法与求最小值/最大值的融合运算:
三个参数,将(第一个 + 第二个)与第三个进行比较。语义:
max(a + b, c)、min(a + b, c)
int __viaddmax_s32 ( int, int, int);
unsigned __viaddmax_s16x2(unsigned, unsigned, unsigned);
unsigned __viaddmax_u32 (unsigned, unsigned, unsigned);
unsigned __viaddmax_u16x2(unsigned, unsigned, unsigned);
int __viaddmin_s32 ( int, int, int);
unsigned __viaddmin_s16x2(unsigned, unsigned, unsigned);
unsigned __viaddmin_u32 (unsigned, unsigned, unsigned);
unsigned __viaddmin_u16x2(unsigned, unsigned, unsigned);
三个参数,带 ReLU,将(第一个 + 第二个)与第三个以及零进行比较。语义:
max(a + b, c, 0)、max(min(a + b, c), 0)
int __viaddmax_s32_relu ( int, int, int);
unsigned __viaddmax_s16x2_relu(unsigned, unsigned, unsigned);
int __viaddmin_s32_relu ( int, int, int);
unsigned __viaddmin_s16x2_relu(unsigned, unsigned, unsigned);
这些指令根据计算能力采用硬件加速或软件模拟。有关计算能力要求,请参阅算术指令一节。
完整的 API 可在CUDA Math API 文档中找到。
DPX 是实现动态规划算法的非常有用的工具,例如基因组学中的 Smith-Waterman 和 Needleman-Wunsch 算法,以及路线优化中的 Floyd-Warshall 算法。
三个有符号 32 位整数的最大值,带 ReLU:
int a = -15;
int b = 8;
int c = 5;
int max_value_0 = __vimax3_s32_relu(a, b, c); // max(-15, 8, 5, 0) = 8
int d = -2;
int e = -4;
int max_value_1 = __vimax3_s32_relu(a, d, e); // max(-15, -2, -4, 0) = 0
两个 32 位有符号整数之和、另一个 32 位有符号整数以及零三者中的最小值(ReLU):
int a = -5;
int b = 6;
int c = -2;
int max_value_0 = __viaddmax_s32_relu(a, b, c); // max(-5 + 6, -2, 0) = max(1, -2, 0) = 1
int d = 4;
int max_value_1 = __viaddmax_s32_relu(a, d, c); // max(-5 + 4, -2, 0) = max(-1, -2, 0) = 0
两个无符号 32 位整数的最小值,并确定哪个值较小:
unsigned a = 9;
unsigned b = 6;
bool smaller_value;
unsigned min_value = __vibmin_u32(a, b, &smaller_value); // min_value is 6, smaller_value is true
三对无符号 16 位整数的最大值:
unsigned a = 0x00050002;
unsigned b = 0x00070004;
unsigned c = 0x00020006;
unsigned max_value = __vimax3_u16x2(a, b, c); // max(5, 7, 2) and max(2, 4, 6), so max_value is 0x00070006
5.4.9. 编译器优化提示
编译器优化提示用附加信息修饰代码,以帮助编译器优化生成的代码。
这些内建函数在 SIMT 设备代码中始终可用,但在分块代码中不受支持。
对主机代码的支持取决于主机编译器。
5.4.9.1. #pragma unroll
默认情况下,编译器会展开迭代次数已知的小型循环。不过,可以使用#pragma unroll指令来控制任意给定循环的展开。该指令必须紧接在循环之前,且仅对该循环生效。
后面可以选择跟随一个整型常量表达式。整型常量表达式的各种情况如下:
如果未提供该表达式,且循环的迭代次数为常量,则循环将被完全展开。
如果该表达式的求值结果为
0或1,则循环不会被展开。如果该表达式为非正整数或大于
INT_MAX,则此 pragma 指令将被忽略,并发出警告。
示例:
struct MyStruct {
static constexpr int value = 4;
};
inline constexpr int Count = 4;
__device__ void foo(int* p1, int* p2) {
// no argument specified, the loop will be completely unrolled
#pragma unroll
for (int i = 0; i < 12; ++i)
p1[i] += p2[i] * 2;
// unroll value = 5
#pragma unroll (Count + 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 (MyStruct::value)
for (int i = 0; i < 12; ++i)
p1[i] += p2[i] * 16;
// negative value, pragma unroll ignored
#pragma unroll -1
for (int i = 0; i < 12; ++i)
p1[i] += p2[i] * 2;
}
请参阅Compiler Explorer上的示例。
5.4.9.2. __builtin_assume_aligned()
void* __builtin_assume_aligned(const void* ptr, size_t align)
void* __builtin_assume_aligned(const void* ptr, size_t align, <integral type> offset)
这些内建函数使编译器能够假定返回的指针至少按align字节对齐。
三参数版本使编译器能够假定
(char*) ptr - offset至少按align字节对齐。
align必须是二的幂,且为整数字面量。
示例:
void* res1 = __builtin_assume_aligned(ptr, 32); // compiler can assume 'res1' is at least 32-byte aligned
void* res2 = __builtin_assume_aligned(ptr, 32, 8); // compiler can assume 'res2 = (char*) ptr - 8' is at least 32-byte aligned
5.4.9.3. __builtin_assume()和__assume()
void __builtin_assume(bool predicate)
void __assume (bool predicate) // only with Microsoft Compiler
该内建函数使编译器能够假定布尔实参为真。如果该实参在运行时为假,则行为未定义。请注意,如果该实参具有副作用,则行为未指定。
示例:
__device__ bool is_greater_than_zero(int value) {
return value > 0;
}
__device__ bool f(int value) {
__builtin_assume(value > 0);
return is_greater_than_zero(value); // returns true, without evaluating the condition
}
5.4.9.4. __builtin_constant_p()
int __builtin_constant_p(expression)
如果编译器能够确定expression是编译时常量,该内建函数返回1,否则返回0。不会对该表达式求值,因此expression中的副作用不会发生。
示例:
__device__ int select(int value) {
return __builtin_constant_p(value) ? value * 2 : value;
}
__global__ void kernel(int* out) {
#pragma unroll
for (int i = 0; i < 4; ++i) {
out[i] = select(i); // out[i] = i * 2
}
}
5.4.9.5. __builtin_expect()
long __builtin_expect(long input, long expected)
该内建函数告知编译器,预期input等于expected,并返回input的值。它通常用于向编译器提供分支预测信息。其行为类似于 C++20 的[[likely]]和[[unlikely]]属性。
示例:
// indicate to the compiler that likely "var == 0"
if (__builtin_expect(var, 0))
doit();
5.4.9.6. __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();
}
5.4.9.7. 自定义 ABI pragma 指令
#pragma nv_abi指令通过保留函数所用寄存器的数量,使以分离编译模式编译的应用程序能够达到与全程序编译相近的性能。
使用此 pragma 指令的语法如下,其中EXPR表示任意整型常量表达式:
#pragma nv_abi preserve_n_data(EXPR) preserve_n_control(EXPR)
#pragma nv_abi后面的参数均为可选参数,并且可以按任意顺序提供;不过,至少需要一个参数。preserve_n参数限制函数调用期间保留的寄存器数量:preserve_n_data(EXPR)限制数据寄存器的数量。preserve_n_control(EXPR)限制控制寄存器的数量。
#pragma nv_abi指令可以紧接在设备函数声明或定义之前。
#pragma nv_abi preserve_n_data(16)
__device__ void dev_func();
#pragma nv_abi preserve_n_data(16) preserve_n_control(8)
__device__ int dev_func() {
return 0;
}
或者,也可以将它直接放在设备函数内部 C++ 表达式语句中的间接函数调用之前。请注意,虽然支持对自由函数的间接函数调用,但不支持对函数引用或类成员函数的间接调用。
__device__ int dev_func1();
struct MyStruct {
__device__ int member_func2();
};
__device__ void test() {
auto* dev_func_ptr = &dev_func1; // type: int (*)(void)
#pragma nv_abi preserve_n_control(8)
int v1 = dev_func_ptr(); // CORRECT, indirect call
#pragma nv_abi preserve_n_control(8)
int v2 = dev_func1(); // WRONG, direct call; the pragma has no effect
// dev_func1 has type: int(void)
auto& dev_func_ref = &dev_func1; // type: int (&)(void)
#pragma nv_abi preserve_n_control(8)
int v3 = dev_func_ref(); // WRONG, call to a reference
// the pragma has no effect
auto member_function_ptr = &MyStruct::member_func2; // type: int (MyStruct::*)(void)
#pragma nv_abi preserve_n_control(8)
int v4 = member_function_ptr(); // WRONG, indirect call to member function
// the pragma has no effect
}
当此 pragma 指令应用于设备函数的声明或定义时,它会修改对该函数的任何调用的自定义 ABI 属性。当它放置在间接函数调用位置时,仅影响该特定调用的 ABI 属性。请注意,放置在调用位置时,此 pragma 指令仅影响间接函数调用;它对直接函数调用没有影响。
#pragma nv_abi preserve_n_control(8)
__device__ int dev_func3();
__device__ int dev_func4();
__device__ void test() {
int v1 = dev_func3(); // CORRECT, the pragma affects the direct call
auto* dev_func_ptr = &dev_func4; // type: int (*)(void)
#pragma nv_abi preserve_n_control(8)
int v2 = dev_func_ptr(); // CORRECT, the pragma affects the indirect call
int v3 = dev_func_ptr(); // WRONG, the pragma has no effect
}
请注意,如果函数声明及其对应定义的 pragma 指令参数不匹配,则程序是非良构的。
5.4.9.8. MMA 吞吐量 pragma 指令
nv_mma_throughput pragma 指令用于启用专门针对矩阵乘加运算调优的编译器优化。它仅在入口函数作用域内有效,即仅对__global__函数有效。通过沿调用路径传播的优化,其影响可能扩展到从该入口函数调用的__device__函数。
示例:
#pragma nv_mma_throughput
__global__ void kernel(){
...
}
注意
nv_mma_throughput pragma 指令仍处于实验阶段。它所启用的设置是在一组数量有限的 NVIDIA 内部工作负载上调优的。不保证每个核函数的性能都会得到提升。
5.4.10. 调试与诊断
5.4.10.1. 断言
#define assert(expression) /* unspecified */
如果expression等于零,assert()宏会停止核函数执行。如果程序在调试器中运行,则会触发断点,允许使用调试器检查设备的当前状态。否则,expression等于零的每个线程都会在通过cudaDeviceSynchronize()、cudaStreamSynchronize()或cudaEventSynchronize()与主机同步后,向 stderr 输出一条消息。该消息的格式如下:
<filename>:<line number>:<function>:
block: [blockIdx.x,blockIdx.y,blockIdx.z],
thread: [threadIdx.x,threadIdx.y,threadIdx.z]
Assertion `<expression>` failed.
核函数的执行会中止,并在主机程序中引发中断。assert()宏会导致 CUDA 上下文损坏,使任何后续的 CUDA 调用或核函数调用均以cudaErrorAssert失败。
如果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(void) {
testAssert<<<1,1>>>();
cudaDeviceSynchronize();
return 0;
}
将输出:
test.cu:11: void testAssert(): block: [0,0,0], thread: [0,0,0] Assertion `should_be_one` failed.
断言用于调试。由于它们可能影响性能,建议在生产代码中禁用断言。可以在包含assert.h或<cassert>之前定义NDEBUG预处理器宏,或使用编译器标志-DNDEBUG,以在编译时禁用断言。请注意,表达式不应具有副作用;否则,禁用断言将影响代码的功能。
assert()宏在__device__和__tile__代码中均可用。
5.4.10.2. 断点函数
可以从任意设备线程调用__brkpt()函数来挂起核函数的执行。
void __brkpt();
5.4.10.3. 诊断 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_number1>, <error_number2> ...
使用警告消息中显示的错误编号来指定受影响的诊断。任何诊断都可以更改为错误,但只有警告才能在更改为错误后抑制或恢复其严重级别。nv_diag_default pragma 指令将诊断的严重级别恢复到发出任何其他 pragma 指令之前生效的级别,即经过任何命令行选项修改后的消息正常严重级别。以下示例抑制foo()的declared but never referenced警告:
#pragma nv_diag_suppress 177 // "declared but never referenced"
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 // "declared but never referenced"
void foo() {
int i = 0;
}
#pragma nv_diagnostic pop
void bar() {
int i = 0; // raise a warning
}
请注意,这些指令仅影响nvcc CUDA 前端编译器。它们对主机编译器没有影响。
当支持诊断 pragma 指令时,nvcc会定义宏__NVCC_DIAG_PRAGMA_SUPPORT__。
5.4.11. 线程束矩阵函数
C++ 线程束矩阵运算利用张量核心加速求解形式为D=A*B+C的矩阵问题。在计算能力为 7.0 或更高的设备上,支持对混合精度浮点数据执行这些运算。这需要一个线程束中的所有线程相互协作。此外,只有当整个线程束中条件的求值结果完全相同时,才允许在条件代码中使用这些运算,否则代码执行很可能挂起。
5.4.11.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一个重载类,包含分布在线程束所有线程中的一部分矩阵。矩阵元素到
fragment内部存储的映射未指定,并且可能在未来架构中发生变化。
仅允许某些特定的模板实参组合。第一个模板形参指定片段将如何参与矩阵运算。Use可接受的值为:
当片段用作第一个乘数
A时,为matrix_a,当片段用作第二个乘数
B时,为matrix_b,或者当片段用作源累加器或目标累加器(分别为
C或D)时,为accumulator。m、n和k尺寸描述了参与乘加运算的线程束范围的矩阵分块的形状。每个分块的维度取决于其角色。对于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等待线程束中的所有通道都到达 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的所有模板参数都必须相同。线程束中的所有线程都必须调用此函数,否则结果未定义。store_matrix_sync等待线程束中的所有通道都到达 store_matrix_sync,然后将矩阵片段 a 存储到内存。
mptr必须是一个按 256 位对齐的指针,指向内存中矩阵的第一个元素。ldm描述连续行(对于行主序布局)或连续列(对于列主序布局)之间以元素为单位的步长;对于__half元素类型,它必须是 8 的倍数,对于float元素类型,它必须是 4 的倍数。(即在这两种情况下都是 16 字节的倍数)。输出矩阵的布局必须指定为mem_row_major或mem_col_major。对于线程束中的所有线程,mptr、ldm、layout的值以及 a 的所有模板参数都必须相同。fill_fragment用常量值
v填充矩阵片段。由于矩阵元素到各个片段的映射未作规定,因此通常由线程束中的所有线程使用相同的v值调用此函数。mma_sync等待线程束中的所有通道都到达 mma_sync,然后执行线程束同步的矩阵乘加运算
D=A*B+C。也支持原地运算C=A*B+C。对于线程束中的所有线程,satf的值和每个矩阵片段的模板参数都必须相同。此外,片段A、B、C和D之间的模板参数m、n和k必须匹配。线程束中的所有线程都必须调用此函数,否则结果未定义。
如果satf(饱和至有限值)模式为true,则目标累加器还适用以下数值属性:
如果某个元素的结果为正无穷大,则相应的累加器将包含
+MAX_NORM如果某个元素的结果为负无穷大,则相应的累加器将包含
-MAX_NORM如果某个元素的结果为 NaN,则相应的累加器将包含
+0
由于矩阵元素到每个线程的fragment的映射未作规定,因此必须在调用store_matrix_sync之后从内存(共享内存或全局内存)访问单个矩阵元素。在一种特殊情况下,即线程束中的所有线程都将对所有片段元素统一执行逐元素运算时,可以使用以下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;
5.4.11.2. 替代浮点格式
在计算能力为 8.0 及更高版本的设备上,张量核心支持替代类型的浮点运算。
__nv_bfloat16此数据格式是一种替代的 fp16 格式,其范围与 f32 相同,但精度较低(7 位)。您可以直接通过
cuda_bf16.h中提供的__nv_bfloat16类型使用此数据格式。数据类型为__nv_bfloat16的矩阵片段必须与float类型的累加器组合使用。支持的形状和运算与__half相同。tf32此数据格式是张量核心支持的一种特殊浮点格式,其范围与 f32 相同,但精度较低(>=10 位)。此格式的内部布局由实现定义。要在 WMMA 运算中使用此浮点格式,必须手动将输入矩阵转换为 tf32 精度。
为便于转换,提供了一个新的内建函数
__float_to_tf32。虽然此内建函数的输入和输出实参都是float类型,但输出在数值上将为tf32。这种新精度仅用于张量核心,如果与其他float类型运算混用,结果的精度和范围将是未定义的。一旦输入矩阵(
matrix_a或matrix_b)转换为 tf32 精度,将具有precision::tf32精度的fragment与传给load_matrix_sync的float数据类型组合使用,即可利用这一新功能。两个累加器片段的数据类型都必须为float。唯一支持的矩阵大小是 16x16x8(m-n-k)。片段中的元素表示为
float,因此从element_type<T>到storage_element_type<T>的映射为:precision::tf32 -> float
5.4.11.3. 双精度
在计算能力为 8.0 及更高版本的设备上,张量核心支持双精度浮点运算。要使用这一新功能,必须使用具有double类型的fragment。mma_sync运算将使用 .rn(舍入到最近偶数)舍入修饰符执行。
5.4.11.4. 子字节运算
子字节 WMMA 运算提供了访问张量核心低精度功能的方式。它们被视为预览功能,即其数据结构和 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
experimental::precision::b1,其中bmmaBitOp设置为bmmaBitOpXOR
bmma_sync等待线程束中的所有通道都执行了 bmma_sync,然后执行线程束同步的位矩阵乘加运算
D = (A op B) + C,其中op由逻辑运算bmmaBitOp及其后由bmmaAccumulateOp定义的累加运算组成。可用的运算包括:bmmaBitOpXOR,对matrix_a中的一行与matrix_b的 128 位列执行 128 位异或运算bmmaBitOpAND,对matrix_a中的一行与matrix_b的 128 位列执行 128 位与运算,可在计算能力为 8.0 及更高版本的设备上使用。累加运算始终为
bmmaAccumulateOpPOPC,用于统计置位的位数。
5.4.11.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 原生代码。
5.4.11.6. 元素类型和矩阵大小
张量核心支持多种元素类型和矩阵大小。下表列出了所支持的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 |
5.4.11.7. 示例
以下代码在单个线程束中实现了 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);
}