4.4. 协作组
4.4.1. 简介
协作组是 CUDA 编程模型的扩展,用于组织相互协作的线程组。协作组允许开发者控制线程协作的粒度,帮助他们表达更丰富、更高效的并行分解。协作组还提供扫描和并行归约等常见并行原语的实现。
过去,CUDA 编程模型提供了一种单一且简单的结构来同步相互协作的线程:一个覆盖线程块中所有线程的屏障,由__syncthreads()内建函数实现。为了表达更广泛的并行交互模式,许多以性能为导向的程序员不得不自行编写专用且不安全的原语,用于同步单个线程束内的线程,或在单个 GPU 上运行的多个线程块之间同步线程。尽管所取得的性能提升往往很有价值,但这也导致脆弱代码不断累积,随着时间推移和 GPU 代际更迭,编写、调优和维护这些代码的成本都很高。协作组提供了一种安全且面向未来的机制,用于编写高性能代码。
完整的协作组 API 可参见协作组 API。
4.4.2. 协作组句柄与成员函数
协作组通过协作组句柄进行管理。协作组句柄使参与线程能够获知自身在组中的位置、组大小以及其他组信息。下表列出了一些组成员函数。
访问器 |
返回值 |
|---|---|
|
调用线程的秩。 |
|
组中的线程总数。 |
|
线程在已启动的线程块内的三维索引。 |
|
已启动的线程块的三维尺寸,以线程为单位。 |
完整的成员函数列表可参见协作组 API。
4.4.3. 默认行为/无组执行
表示网格和线程块的组会根据核函数启动配置隐式创建。这些“隐式”组提供了一个起点,开发者可以将其显式分解为粒度更细的组。可以使用以下方法访问隐式组:
访问器 |
组作用域 |
|---|---|
|
返回包含当前线程块中所有线程的组的句柄。 |
|
返回包含网格中所有线程的组的句柄。 |
|
返回由线程束中当前活跃线程组成的组的句柄。 |
|
返回由当前簇中的线程组成的组的句柄。 |
更多信息可参见协作组 API。
4.4.3.1. 尽早创建隐式组句柄
为了获得最佳性能,建议您预先创建隐式组的句柄(尽可能早,在任何分支发生之前),并在整个核函数中使用该句柄。
4.4.3.2. 仅按引用传递组句柄
将组句柄传入函数时,建议按引用向函数传递组句柄。由于没有默认构造函数,组句柄必须在声明时初始化。不建议通过复制构造来创建组句柄。
4.4.4. 创建协作组
组通过将父组划分为子组来创建。划分组时,会创建一个组句柄来管理得到的子组。开发者可以使用以下划分操作:
划分类型 |
描述 |
|---|---|
tiled_partition |
将父组划分为一系列大小固定的子组,这些子组以一维行主序格式排列。 |
labeled_partition |
根据条件标签将父组划分为一维子组,标签可以是任意整型。 |
binary_partition |
标签划分的一种特化形式,其中标签只能为“0”或“1”。 |
以下示例展示了如何创建分块划分:
namespace cg = cooperative_groups;
// Obtain the current thread's cooperative group
cg::thread_block my_group = cg::this_thread_block();
// Partition the cooperative group into tiles of size 8
cg::thread_block_tile<8> my_subgroup = cg::tiled_partition<8>(my_group);
// do work as my_subgroup
最适合使用的划分策略取决于具体情境。更多信息请参阅协作组 API。
4.4.4.1. 避免组创建风险
划分组是一项集合操作,组中的所有线程都必须参与。如果在并非所有线程都会到达的条件分支中创建组,可能导致死锁或数据损坏。
4.4.5. 同步
在引入协作组之前,CUDA 编程模型只允许在核函数完成边界处进行线程块间同步。协作组允许开发者以不同的粒度同步由协作线程组成的组。
4.4.5.1. 同步
可以通过调用集合函数sync()来同步一个组。与__syncthreads()一样,sync()函数提供以下保证:
组中线程在同步点之前进行的所有内存访问(例如读取和写入),在同步点之后对组中所有线程都可见。
组中的所有线程都到达同步点之后,才允许任何线程越过该同步点继续执行。
以下示例展示了与__syncthreads()等效的cooperative_groups::sync()。
namespace cg = cooperative_groups;
cg::thread_block my_group = cg::this_thread_block();
// Synchronize threads in the block
cg::sync(my_group);
协作组可用于同步整个网格。从 CUDA 13 起,协作组不再能用于多设备同步。有关详细信息,请参阅大规模组一节。
有关同步的更多信息,请参阅协作组 API。
4.4.5.2. 屏障
协作组提供了一个类似于cuda::barrier的屏障 API,可用于更高级的同步。协作组屏障 API 与cuda::barrier在以下几个关键方面有所不同:
协作组屏障会自动初始化
组中的所有线程在每个阶段都必须到达屏障并在屏障处等待一次。
barrier_arrive返回一个arrival_token对象,该对象必须传入对应的barrier_wait,并在其中被消耗,此后不能再次使用。
程序员在使用协作组屏障时必须注意避免以下风险:
在调用
barrier_arrive之后、调用barrier_wait之前,组不能使用任何集合操作。barrier_wait仅保证组中的所有线程都已调用barrier_arrive。barrier_wait并不保证所有线程都已调用barrier_wait。
namespace cg = cooperative_groups;
cg::thread_block my_group = this_block();
cg::cluster_group cluster = this_cluster();
auto token = cluster.barrier_arrive();
// Optional: Do some local processing to hide the synchronization latency
local_processing(my_group);
// Make sure all other blocks in the cluster are running and initialized shared data before accessing dsmem
cluster.barrier_wait(std::move(token));
4.4.6. 集合操作
协作组包含一组可由一组线程执行的集合操作。这些操作需要指定组中的所有线程参与才能完成。
对于每次集合调用,组中的所有线程都必须为对应的参数传入相同的值,除非协作组 API明确允许不同的值。否则,该调用的行为未定义。
4.4.6.1. 归约
reduce函数用于对指定组中每个线程提供的数据执行并行归约。必须提供下表所示运算符之一来指定归约类型。
运算符 |
返回值 |
|---|---|
plus |
组中所有值的总和 |
less |
最小值 |
greater |
最大值 |
bit_and |
按位与归约 |
bit_or |
按位或归约 |
bit_xor |
按位异或归约 |
归约会在硬件加速可用时使用硬件加速(要求计算能力为 8.0 或更高)。对于不具备硬件加速的较旧硬件,可以使用软件回退实现。只有 4 字节类型可以获得硬件加速。
有关归约的更多信息,请参阅协作组 API。
以下示例展示了如何使用cooperative_groups::reduce()在线程块范围内执行求和归约。
namespace cg = cooperative_groups;
cg::thread_block my_group = cg::this_thread_block();
int val = data[threadIdx.x];
int sum = cg::reduce(my_group, val, cg::plus<int>());
// Store the result from the reduction
if (my_group.thread_rank() == 0) {
result[blockIdx.x] = sum;
}
4.4.6.2. 扫描
协作组包含可用于任意组大小的inclusive_scan和exclusive_scan实现。这些函数对指定组所包含的每个线程提供的数据执行扫描操作。
程序员可以选择指定上面的归约运算符表中列出的归约运算符。
namespace cg = cooperative_groups;
cg::thread_block my_group = cg::this_thread_block();
int val = data[my_group.thread_rank()];
int exclusive_sum = cg::exclusive_scan(my_group, val, cg::plus<int>());
result[my_group.thread_rank()] = exclusive_sum;
有关扫描的更多信息,请参阅协作组扫描 API。
4.4.6.3. 单线程调用
协作组提供了一个invoke_one函数,用于必须由单个线程代表一个组执行工作中的串行部分的情况。
invoke_one从调用组中任意选择一个线程,并使用该线程以所提供的参数调用所提供的可调用函数。invoke_one_broadcast与invoke_one相同,只是调用结果还会广播到组中的所有线程。
不保证线程选择机制具有确定性。
以下示例展示了invoke_one的基本用法。
namespace cg = cooperative_groups;
cg::thread_block my_group = cg::this_thread_block();
// Ensure only one thread in the thread block prints the message
cg::invoke_one(my_group, []() {
printf("Hello from one thread in the block!");
});
// Synchronize to make sure all threads wait until the message is printed
cg::sync(my_group);
可调用函数内部不允许在调用组内进行通信或同步。允许与调用组之外的线程进行通信。
4.4.7. 异步数据移动
CUDA 中协作组的memcpy_async功能提供了一种在全局内存与共享内存之间执行异步内存复制的方法。memcpy_async尤其适合优化内存传输,并使计算与数据传输重叠,从而提高性能。
memcpy_async函数用于启动从全局内存到共享内存的异步加载。memcpy_async旨在像“预取”一样使用,即在需要数据之前将其加载。
wait函数强制组中的所有线程等待,直到异步内存传输完成。组中的所有线程都必须调用wait,然后才能访问共享内存中的数据。
以下示例展示了如何使用memcpy_async和wait预取数据。
namespace cg = cooperative_groups;
cg::thread_group my_group = cg::this_thread_block();
__shared__ int shared_data[];
// Perform an asynchronous copy from global memory to shared memory
cg::memcpy_async(my_group, shared_data + my_group.rank(), input + my_group.rank(), sizeof(int));
// Hide latency by doing work here. Cannot use shared_data
// Wait for the asynchronous copy to complete
cg::wait(my_group);
// Prefetched data is now available
更多信息请参阅协作组 API。
4.4.7.1. 异步内存复制的对齐要求
只有当源为全局内存、目标为共享内存,且两者均至少按 4 字节对齐时,memcpy_async才是异步的。为获得最佳性能,建议共享内存和全局内存均按 16 字节对齐。
4.4.8. 大规模组
协作组允许使用跨越整个网格的大规模组。前面介绍的所有协作组功能都可用于这些大规模组,但有一个显著的例外:同步整个网格需要使用cudaLaunchCooperativeKernel运行时启动 API。
从 CUDA 13 起,协作组的多设备启动 API 及相关引用已被移除。
4.4.8.1. 何时使用cudaLaunchCooperativeKernel
cudaLaunchCooperativeKernel是一个 CUDA 运行时 API 函数,用于启动使用协作组的单设备核函数,专为执行需要线程块间同步的核函数而设计。此函数确保核函数中的所有线程都能在整个网格范围内同步并协作,而传统 CUDA 核函数只允许在各个线程块内部同步,无法做到这一点。cudaLaunchCooperativeKernel确保核函数启动是原子的,即如果 API 调用成功,则指定数量的线程块将在指定设备上启动。
良好的做法是首先通过查询设备属性cudaDevAttrCooperativeLaunch来确保设备支持协作启动:
int dev = 0;
int supportsCoopLaunch = 0;
cudaDeviceGetAttribute(&supportsCoopLaunch, cudaDevAttrCooperativeLaunch, dev);
如果设备 0 支持该属性,这会将supportsCoopLaunch设置为 1。仅支持计算能力为 6.0 及更高的设备。此外,还需要在以下环境之一中运行:
不使用 MPS 的 Linux 平台
使用 MPS 且设备计算能力为 7.0 或更高的 Linux 平台
最新的 Windows 平台