Cooperative Groups

Cooperative Groups(协同组)是CUDA 9.0引入的一个新概念,主要用于跨线程块(block)的同步。为使用Cooperative Groups,我们需要包含头文件#include <cooperative_groups.h>,同时需要cooperative_groups命名空间。

简介

在CUDA 9.0之前,CUDA仅支持线程块内的同步,CUDA提供了2个原语操作:__syncthreads()函数用于同步同一线程块内的所有线程,以及__syncwarp(unsigned mask=0xffffffff)函数用于同步线程束内的线程。

附1:

由于__syncthreads()函数要求整个线程块内的所有线程都得到达该同步点方能继续执行,也就是说同一线程块的if条件必须都相同,否则程序将会被挂起或产生意想不到的结果。为避免此问题,CUDA提供了如下三个函数用于评估if条件的预测值:

  1. int __syncthreads_count(int predicate);

    该函数在__syncthreads()函数基础上增加了返回predicate值非0的线程的数目。

  2. int __syncthreads_and(int predicate);

该函数在__syncthreads()函数基础上,当且仅当块内所有线程predicate值非0时返回一个非0值。

  1. int __syncthreads_or(int predicate);

    该函数在__syncthreads()函数基础上,当且仅当块内存在任意一个线程predicate值非0时返回一个非0值。

显然,线程块级的同步并不能满足开发者的需求,在某些时候,开发者需要跨线程块同步,针对此问题,CUDA 9.0推出了Cooperative Groups机制,用于线程块内和跨线程块的同步。该机制为开发者提供了自定义线程组的方式,并提供了相应的同步函数,同时还包括一个新的kernel启动API(cudaLaunchCooperativeKernel),该API保证了Cooperative Groups同步的安全性。

块内组

thread_block

Cooperative Groups引入了一个新的数据结构:thread_block,即线程块。thread_block可以通过this_thread_block()进行获取并初始化:

thread_block g = this_thread_block();

thread_block继承自更广义的线程组数据结构:thread_group 。thread_group 提供了如下函数:

void sync(); //同步组内的所有线程,这里g.sync()等价于__syncthreads()
unsigned size(); //获取组内的线程数目
unsigned thread_rank(); //获取线程的组内索引值([0,size])
bool is_valid(); //判断本组是否违背了任何APIconstraints(API限制)

thread_block则提供如下特定线程块函数:

dim3 group_index();  //网格grid内3维索引(block索引)
dim3 thread_index(); //块block内3维索引(线程索引)

注意以上所有操作组内所有线程都要确保执行到,否则行为未定义。

相比__syncthreads()函数,使用g.sync()的好处在于避免了隐式同步隐患:

__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);
}

此时,当开发者调用他人编写的sum函数时,不一定能发现sum中存在着同步,但当我们显式传参时情况就不一样了:

__device__ int sum(const thread_group& g, int *x, int n)
{
// ...
g.sync()
return total;
}
__global__ void parallel_kernel(float *x)
{
// ...
// Entire thread block must call sum
sum(this_thread_block(), x, n);
}

tiled_partition

tiled_partition()函数用于将一个线程块分解为多个小的协同线程组(tiled subgroups),比如说:

thread_block wholeBlock = this_thread_block(); //获取线程块

以下函数将线程块分解为若干个大小为32的小线程组:

thread_group tile32 = tiled_partition(wholeBlock, 32);

甚至可以更深一步,将tile32分解为更小的若干个大小为4的小线程组:

thread_group tile4 = tiled_partition(tile32, 4);

注意:小线程组大小仅支持2的幂数且不大于32,也就是仅限于2,4,8,16,32

现在,我们通过如下操作就可以让线程0,4,8,12,...(相对于wholeBlock 的索引)打印"Hello World":

if (tile4.thread_rank() == 0) printf("Hello World\n");

Thread Block Tiles

CUDA还提供了thread_block_tile<>模版使得小线程组大小在编译期就可以得到:

thread_block wholeBlock = this_thread_block(); //获取线程块
thread_block_tile<32> tile32 = tiled_partition<32>(wholeBlock);
thread_block_tile<4> tile4 = tiled_partition<4>(tile32);

Thread Block Tiles提供了如下成员函数用于协同同步:

.shfl()  //等价__shfl_sync
.shfl_down() //等价__shfl_down_sync
.shfl_up() //等价__shfl_up_sync
.shfl_xor() //等价__shfl_xor_sync
.any() //等价__any_sync
.all() //等价__all_sync
.ballot() //等价__ballot_sync
.match_any() //等价__match_any_sync
.match_all() //等价__match_all_sync

注意相比通过tiled_partition()函数传参动态设置线程组大小,通过tiled_partition<>模版静态设置线程组大小使得开发者可以使用如上这些线程束同步函数,前者不能。

附2:

__shfl_sync系列指令(俗称洗牌指令)用于在线程束中获取指定线程的变量值,该操作会在mask(一般取0xffffffff,每个bit位代表每个线程id)指定的那些线程中同时执行(同一mask中的线程必须执行相同指令),每次移动4字节或8字节的数据,但若指定线程为非活跃线程,则结果未知。具体功能如下:

T __shfl_sync(unsigned mask, T var, int srcLane, int width=warpSize);

__shfl_sync指令返回索引为srcLane线程的var变量值,其中srcLane大小为[0,width),类似的,width的值必须是2的幂数且不大于32。

T __shfl_up_sync(unsigned mask, T var, unsigned int delta, int width=warpSize);

__shfl_up_sync指令返回索引为当前线程索引减去delta的值的线程的var值,若减去后的值小于0则不做任何操作(保持不变)。

T __shfl_down_sync(unsigned mask, T var, unsigned int delta, int width=warpSize);

__shfl_down_sync指令返回索引为当前线程索引加上delta的值的线程的var值,若加后的值大于width则不做任何操作(保持不变)。

T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width=warpSize);

__shfl_xor_sync指令返回索引为当前线程索引按位异或laneMask后的值的线程的var值。注意若width值小于warpSize值,此时后面的线程可以访问前面的线程组的值(获取成功),但前面的线程不能访问后面线程组的值(保持不变)。

附3:

__any_sync系列指令(俗称投票指令)对线程束中的参与线程(同样由mask指定)比较预测值predicate是否非零,并向所有参与的活跃线程广播比较结果:

int __all_sync(unsigned mask, int predicate);

当线程束中所有参与线程的预测值predicate非零时返回一个非零值。

int __any_sync(unsigned mask, int predicate);

当线程束中存在任意一个参与线程的预测值predicate非零时返回一个非零值。

unsigned __ballot_sync(unsigned mask, int predicate);

若线程束中的第N个线程活跃且其预测值predicate非零时,设定返回值的第N个bit为1,否则为0。

unsigned __activemask();

返回线程束内活跃线程组成的掩码。若线程束中的第N个线程为活跃线程,则设定第N个bit为1,否则为0(注意已退出线程也是非活跃线程)。该指令不执行同步。

附4:

__match_any_sync系列指令对线程束的参与线程(同样由mask指定)比较value值,并向所有参与线程广播比较结果:

unsigned int __match_any_sync(unsigned mask, T value);

返回value值相同的那些线程组成的掩码。

unsigned int __match_all_sync(unsigned mask, T value, int *pred);

返回mask值若所有参与线程的value值都相同,否则返回0。此外前者的预测值pred还将被设定为true,否则为false。

Coalesced Groups

若同一线程束(warp)内的线程出现条件分化(通常由if语句导致),那么程序将序列化运行:既在执行某分支线程时停止其它分支线程的执行,直到所有分支执行完毕。我们称正执行的活跃线程为coalesced thread,线程束内所有活跃线程组成的线程组即为coalesced groups,其可以通过coalesced_threads函数获取:

coalesced_group active = coalesced_threads();

coalesced_group也是一类thread_group。

网格级同步

相比块内组,Cooperative Groups最强大的能力在于跨线程块同步,在CUDA 9.0之前,不同线程块仅能在kernel执行结束时同步,现在开发者可以通过grid_group 结构执行网格级同步:

grid_group grid = this_grid();
grid.sync();

注意不同于传统的<<<...>>>执行配置,网格级同步必须通过cudaLaunchCooperativeKernel API配置并启动kernel:

cudaError_t cudaLaunchCooperativeKernel(
const T *func, //kernel函数指针
dim3 gridDim,
dim3 blockDim,
void **args, //kernel参数数组
size_t sharedMem = 0,
cudaStream_t stream = 0
)

注意为保证所有协同线程块能安全的常驻GPUgridDimblockDim的值需要慎重考虑,开发者可以通过计算SM的最大活跃线程块数目来最大化并行率:

cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&numBlocksPerSm,
my_kernel,
numThreads,
0
);
// initialize, then launch
cudaLaunchCooperativeKernel(
(void*)my_kernel,
deviceProp.multiProcessorCount*numBlocksPerSm,
numThreads,
args
);

Cooperative Launch目前不支持任务抢占和调度,若一次启动的block数超过了设备驻留的极限,则报错too many blocks in cooperative launch cudaLaunchCooperativeKernel,此时你需要检查一下启动block数、使用的共享内存大小、使用的寄存器大小。相关问题见(https://bbs.gpuworld.cn/index.php?topic=73127.0)

除特殊的启动函数外,网格同步还需要在编译时开启-rdc=true参数。

该功能仅支持计算能力6.0及以上的设备,在不确定GPU是否支持网格同步时,开发者可以通过如下方式查询:

int pi=0;
cuDevice dev;
cuDeviceGet(&dev,0) // get handle to device 0
cuDeviceGetAttribute(&pi, CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH, dev);

当pi值为1时表明设备0支持网格级同步。

多设备同步

类似网格级同步,多设备同步通过multi_grid_group 结构执行:

multi_grid_group multi_grid = this_multi_grid();
multi_grid.sync();

并通过cudaLaunchCooperativeKernelMultiDevice API配置并启动kernel:

cudaError_t cudaLaunchCooperativeKernelMultiDevice(
CUDA_LAUNCH_PARAMS *launchParamsList,
unsigned int numDevices,
unsigned int flags = 0
);

其中CUDA_LAUNCH_PARAMS结构体定义如下:

typedef struct CUDA_LAUNCH_PARAMS_st {
CUfunction function;
unsigned int gridDimX;
unsigned int gridDimY;
unsigned int gridDimZ;
unsigned int blockDimX;
unsigned int blockDimY;
unsigned int blockDimZ;
unsigned int sharedMemBytes;
CUstream hStream;
void **kernelParams;
} CUDA_LAUNCH_PARAMS;

当开发者使用该API需要注意如下几点:

  1. 该API将确保一个launch操作是原子的,例如当API调用成功时,相应数目的线程块在所有指定设备上launch成功。

  2. 对于所有设备,该API调用的kernel函数必须是相同的。

  3. 同一设备上的launchParamsList参数必须是相同的。

  4. 所有设备的计算能力必须是相同的(major and minor versions)。

  5. 对于所有设备,配置的网格大小(gridDim)、块大小(blockDim)和每个网格的共享内存大小必须是相同的。

  6. 自定义的__device__,__constant__,__managed__全局变量在每个设备上都是独立实例化的,因此需要开发者对该类变量赋初值。

类似的,该功能仅支持计算能力6.0及以上设备,可以通过CU_DEVICE_ATTRIBUTE_COOPERATIVE_MULTI_DEVICE_LAUNCH 查询。

Cooperative Groups的更多相关文章

  1. 内核融合:GPU深度学习的“加速神器”

    ​编者按:在深度学习"红透"半边天的同时,当前很多深度学习框架却面临着共同的性能问题:被频繁调用的代数运算符严重影响模型的执行效率. 本文中,微软亚洲研究院研究员薛继龙将为大家介绍 ...

  2. 使用CUDA Warp-Level级原语

    使用CUDA Warp-Level级原语 NVIDIA GPU以SIMT(单指令,多线程)的方式执行称为warps 的线程组.许多CUDA程序通过利用warp执行来实现高性能.本文将展示如何使用cud ...

  3. CUDA C++编程手册(总论)

    CUDA C++编程手册(总论) CUDA C++ Programming Guide The programming guide to the CUDA model and interface. C ...

  4. iOS: 在iPhone和Apple Watch之间共享数据: App Groups

    我们可以在iPhone和Apple Watch间通过app groups来共享数据.方法如下: 首先要在dev center添加一个新的 app group: 接下来创建一个新的single view ...

  5. [AlwaysOn Availability Groups]AG排查和监控指南

    AG排查和监控指南 1. 排查场景 如下表包含了常用排查的场景.根据被分为几个场景类型,比如Configuration,client connectivity,failover和performance ...

  6. Authorization in Cloud Applications using AD Groups

    If you're a developer of a SaaS application that allows business users to create and share content – ...

  7. iOS项目groups和folder的区别(组和文件夹)

    在引用一个第三方框架的时候,已经拖进去了,但是引用框架里面的文件时,竟然报错说找不到.......查了一下,原来在拖进去时没有注意group和folder的选择! 其实仔细观察一下,不难发现,以gro ...

  8. [AlwaysOn Availability Groups]排查:AG配置

    排查AG配置 本文主要用来帮助排查在AG配置时出现的问题,包括,AG功能被禁用,账号配置不正确,数据库镜像endpoint不存在,endpoint不能访问. Section Description A ...

  9. [AlwaysOn Availability Groups]DMV和系统目录视图

    DMV和系统目录视图 这里主要介绍AlwaysON的动态管理视图,可以用来监控和排查你的AG. 在AlwaysOn Dashboard,你可以简单的配置的GUI显示很多可用副本的DMV和可用数据库通过 ...

随机推荐

  1. RabbitMQ巩固学习一

    说起RabbitMQ大家第一时间应该想到的就是异步队列,关于异步队列的话题简直太多了,各位同学在园子里一搜便知.我第一次听异步队列这个名词感觉非常高大上

  2. 007.OpenShift管理应用部署

    一 REPLICATION CONTROLLERS 1.1 RC概述 RC确保pod指定数量的副本一直运行.如果pod被杀死或被管理员显式删除,复制控制器将自动部署相应的pod.类似地,如果运行的po ...

  3. Java并发编程-深入探讨synchronized实现原理

    synchronized这个关键字对应Java程序猿来说是非常的熟悉,只要遇到要解决线程安全问题的地方都会使用这个关键字.接下来一起来探讨一下synchronized到底时怎么实现线程同步,使用syn ...

  4. CPU明明8个核,网卡为啥拼命折腾一号核?

    中断机制 我是CPU一号车间的阿Q,我又来了! 我们日常的工作就是不断执行代码指令,不过这看似简单的工作背后其实也并不轻松. 咱不能闷着头啥也不管一个劲的只管执行代码,还得和连接在主板上的其他单位打交 ...

  5. 从Spring Initializr开始

    出识springcloud我们这里需要建立两个项目 来感受下微服务 一.配置服务 1. Spring Initializr. 用idea自带的 Spring Initializr. 建立第一个项目 2 ...

  6. 5、struct2的获得jsp参数的第三种方式

    在前面已经讲解了通过在action中直接通过jsp的参数和ModelDiver的方式获得浏览器传递的参数,下面我们介绍第三种方式,也是在项目开发中推荐的方式 action不需要在实现ModelDriv ...

  7. Python 简明教程 --- 12,Python 字典

    微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 代码写的越急,程序跑得越慢. -- Roy Carlson 目录 Python 字典是另一种非常实用 ...

  8. vs code 初始化vue项目框架

    1.首先安装npm组件  下载地址:https://nodejs.org/en/ 安装完 2.配置环境变量 3.验证是否成功 node -v npm -v 4.替换npm 输入npm install ...

  9. 博弈论Nim取子问题,困扰千年的问题一行代码解决

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是算法与数据结构专题26篇文章,我们来看看一个新的博弈论模型--Nim取子问题. 这个博弈问题非常古老,延续长度千年之久,一直到20世纪 ...

  10. AdminLTE 2.4

    https://jeesite.gitee.io/front/AdminLTE/2.4/index2.html