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. Scrapy学习1:安装

    Install Scrapy 熟悉PyPI的话,直接一句 pip install Scrapy 但是有时候需要处理安装依赖,不能直接一句命令就安装结束,这个和系统有关. 我用的Ubuntu,这里仅介绍 ...

  2. CODING DevOps 系列第五课:微服务测试——微服务下展开体系化的微服务测试

    微服务测试的痛点与挑战 这张图可以形象地展示单体服务和微服务的对比,单体应用就像左边巨大的集装箱,软件模块和应用都包括其中:而微服务就像是由一个小集装箱组成,微小的服务组成一个庞大.完整的系统.单体服 ...

  3. python 2 与python 3区别汇总

    python 2 与python 3区别汇总 一.核心类差异1. Python3 对 Unicode 字符的原生支持.Python2 中使用 ASCII 码作为默认编码方式导致 string 有两种类 ...

  4. Docker编写镜像 发布个人网站

    推荐国内镜像中心:网易云镜像----> https://c.163.com/hub#/home  或者歪果镜像---> https://hub.docker.com/ 博客地址:http: ...

  5. 如何理解nginx反向代理,其实叫逆向代理更容易让我理解

    接触nginx后,以我的语文水平,一直无法理解它神奇的名字:反向代理 怎么就反向了?反哪里去了 (以下部分图片.内容来自网络整理) 1.先理解正向代理 正向代理( Forward Proxy ): 客 ...

  6. 从零开始搭建SpringBoot项目

    一.新建springboot项目 1. new-->Project-->Spring Initralizr Group:com.zb Artifact:zbook springboot v ...

  7. 每日一题 - 剑指 Offer 32 - III. 从上到下打印二叉树 III

    题目信息 时间: 2019-06-25 题目链接:Leetcode tag:双端队列 难易程度:中等 题目描述: 请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右 ...

  8. HTML文档解析和DOM树的构建

    浏览器解析HTML文档生成DOM树的过程,以下是一段HTML代码,以此为例来分析解析HTML文档的原理 <!DOCTYPE html> <html lang="en&quo ...

  9. css如何将图片横向平铺?

    在CSS中,可以使用background(背景)属性来添加图片,默认图片是向x轴和y轴重复.那么css如何将图片横向平铺?下面本篇文章就来给大家介绍一下使用CSS将图片横向平铺的方法,希望对大家有所帮 ...

  10. 帝国の狂欢(种树)(可撤销DP)

    题目描述 马上就要开学了!!! 为了给回家的童鞋们接风洗尘,HZOI帝国的老大决定举办一场狂欢舞会. 然而HZOI帝国头顶上的HZ大帝国十分小气,并不愿意给同学们腾出太多的地方.所以留给同学们开par ...