1. 单指令多线程模式

从硬件上看,一个GPU被分为若干个SM。线程块在执行时将被分配到还没完全占满的SM中,一个线程块不会被分配到不同的SM中,一个SM可以有一个或多个线程块。不同线程块之间可以并发或顺序地执行。当某些线程块完成计算任务后,对应的SM会部分或完全地空闲,然后会有新的线程块被分配到空闲的SM。从更细的粒度看,一个SM以32个线程为单位产生、管理、调度、执行线程,这样的32个线程称为一个线程束,每个线程束包含32个具有连续线程号的线程。

在Volta架构之前,一个线程束中的线程拥有同一个程序计数器(program counter),但有各自不同的寄存器状态。在同一时刻,一个线程束中的线程只能执行一个共同的指令或者闲置,这称为单指令多线程(single instruction multiple thread, SIMT)模式。当一个线程束中的线程顺序地执行判断语句中的不同分支时,即发生了分支发散(branch divergence)。例如有如下语句:

if (condition)
{
A;
}
else
{
B;
}

首先,满足condition条件的线程会执行语句A,其它线程闲置;然后,不满足condition条件的线程执行语句B,其它线程闲置。如果语句A和语句B的指令数差不多,则整个线程束的执行效率就会降低一半,所以在编写代码时,应该尽量避免分支发散。需要注意的是,分支发散是针对同一个线程束内部线程的,不同线程束执行条件语句的不同分支则不属于分支发散。

从Volta架构开始,引入了独立线程调度机制,每个线程拥有自己的程序计数器。同时,这也使得假设了线程束同步的代码变得不再安全。如果要在Volta或者更高架构的GPU中运行一个使用了线程束同步假设的程序,可以在编译时将虚拟架构指定为低于Volta架构的计算能力,例如-arch=compute_60 -code=sm_70,这样在生成PTX代码时就使用了Pascal架构的线程调度机制,而忽略了Volta架构的独立线程调度机制。

2. 线程同步

线程块同步函数:

//保证一个线程块中的所有线程(或者说所有线程束)在执行该语句后面的语句之前都执行完了该语句前面的语句
void __syncthreads();

线程束同步函数:

//参数mask是一个代表掩码的无符号整数,默认32个比特位都为1,表示线程束中的所有线程都参与同步,如果要排除一些线程,可以把对应比特位置0,例如0xfffffffe表示排除第0号线程
void __syncwarp(unsigned mask=0xffffffff);

此外,还有一些线程束内的基本函数,它们都具有隐式的同步功能。其中线程束表决函数(warp vote functions)和线程束洗牌函数(warp shuffle functions)自Kepler架构开始就可以使用,但在CUDA 9版本中进行了更新,线程束匹配函数(warp match functions)和线程束矩阵函数(warp matrix functions)只能在Volta或更高架构的GPU中使用。

线程束内基本函数中的参数mask称为掩码,是一个32位的无符号整数,其二进制从右边数起刚好对应线程束内的32个线程。掩码用于指定要参与计算的线程,比特位等于1表示参与计算,比特位等于0表示忽略。各种函数返回的结果对于被掩码排除的线程来说没有定义,所以不要在被排除的线程中使用函数的返回值。

// ================ 线程束表决函数 ================

//如果线程束内第n个线程参与计算且pred值非0,则返回无符号整数的第n个比特位取1,否则取0。该函数相当于从一个旧的掩码产生一个新的掩码。
unsigned __ballot_sync(unsigned mask, int pred); //线程束内所有参与线程的pred值都不为0时才返回1,否则返回0。该函数类似于这样一种选举操作,当所有参选人都同意时才通过。
int __all_sync(unsigned mask, int pred); //线程束内所有参与线程的pred值至少有一个不为0时就返回1,否则返回0。该函数类似于这样一种选举操作,只要有一个参选人同意就通过。
int __any_sync(unsigned mask, int pred); // ================ 线程束洗牌函数 ================
//对于所有洗牌函数,类型T可以是int、long、long long、unsigned、unsigned long、unsigned long long、float、double。
//最后一个参数width默认值为warpSize(即32),且只能取2、4、8、16、32其中的一个,它表示逻辑上线程束的大小。
//标号srcLane指的是当前线程在width范围内的位置,例如当width等于8时,srcLane的范围就是0~7。 //参与线程返回标号为srcLane的线程中变量var的值,即将一个线程的数据广播到所有(包括自己)线程。
T __shfl_sync(unsigned mask, T var, int srcLane, int width); //标号为srcLane的参与线程返回标号为srcLane - delta的线程中变量var的值,标号srcLane < delta的线程返回自己的var值。形象地说,这是一种将数据向上平移的操作。
T __shfl_up_sync(unsigned mask, T var, unsigned int delta, int width); //标号为srcLane的参与线程返回标号为srcLane + delta的线程中变量var的值,标号srcLane >= width - delta的线程返回自己的var值。形象地说,这是一种将数据向下平移的操作。
T __shfl_down_sync(unsigned mask, T var, unsigned int delta, int width); //标号为srcLane的参与线程返回标号为srcLane ^ laneMask的线程中变量var的值,这里的^符号表示整数按位异或的操作。例如width等于8,laneMask等于2时,第0~7号线程分别返回标号为2、3、0、1、6、7、4、5的线程中变量var的值。
T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width)

为了更好地理解上述函数,可以参考下面的测试程序,输出结果在注释中:

#include <cstdio>

const unsigned WIDTH = 8;
const unsigned BLOCK_SIZE = 16;
const unsigned FULL_MASK = 0xffffffff; void __global__ test_warp_primitives(void); int main(int argc, char** argv)
{
test_warp_primitives<<<1, BLOCK_SIZE>>>();
cudaDeviceSynchronize();
return 0;
} void __global__ test_warp_primitives(void)
{
int tid = threadIdx.x;
int lane_id = tid % WIDTH; if (tid == 0) printf("threadIdx.x: ");
printf("%2d ", tid);
if (tid == 0) printf("\n");
// threadIdx.x: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (tid == 0) printf("lane_id: ");
printf("%2d ", lane_id);
if (tid == 0) printf("\n");
// lane_id: 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 unsigned mask1 = __ballot_sync(FULL_MASK, tid > 0);
unsigned mask2 = __ballot_sync(FULL_MASK, tid == 0);
if (tid == 0) printf("FULL_MASK = %x\n", FULL_MASK);
if (tid == 1) printf("mask1 = %x\n", mask1);
if (tid == 0) printf("mask2 = %x\n", mask2);
// FULL_MASK = ffffffff
// mask1 = fffe
// mask2 = 1 int result = __all_sync(FULL_MASK, tid);
if (tid == 0) printf("all_sync (FULL_MASK): %d\n", result);
// all_sync (FULL_MASK): 0 result = __all_sync(mask1, tid);
if (tid == 1) printf("all_sync (mask1): %d\n", result);
// all_sync (mask1): 1 result = __any_sync(FULL_MASK, tid);
if (tid == 0) printf("any_sync (FULL_MASK): %d\n", result);
// any_sync (FULL_MASK): 1 result = __any_sync(mask2, tid);
if (tid == 0) printf("any_sync (mask2): %d\n", result);
// any_sync (mask2): 0 int value = __shfl_sync(FULL_MASK, tid, 2, WIDTH);
if (tid == 0) printf("shfl: ");
printf("%2d ", value);
if (tid == 0) printf("\n");
// shfl: 2 2 2 2 2 2 2 2 10 10 10 10 10 10 10 10 value = __shfl_up_sync(FULL_MASK, tid, 1, WIDTH);
if (tid == 0) printf("shfl_up: ");
printf("%2d ", value);
if (tid == 0) printf("\n");
// shfl_up: 0 0 1 2 3 4 5 6 8 8 9 10 11 12 13 14 value = __shfl_down_sync(FULL_MASK, tid, 1, WIDTH);
if (tid == 0) printf("shfl_down: ");
printf("%2d ", value);
if (tid == 0) printf("\n");
// shfl_down: 1 2 3 4 5 6 7 7 9 10 11 12 13 14 15 15 value = __shfl_xor_sync(FULL_MASK, tid, 1, WIDTH);
if (tid == 0) printf("shfl_xor: ");
printf("%2d ", value);
if (tid == 0) printf("\n");
// shfl_xor: 1 0 3 2 5 4 7 6 9 8 11 10 13 12 15 14
}

3. 协作组

协作组(cooperative groups)可以看作线程块和线程束同步机制的推广,它提供了更为灵活的线程协作方式,包括线程块内部的同步协作、线程块之间(网格级)的同步协作以及设备之间的同步协作,本文只介绍线程块内部的协作组。协作组由CUDA 9引入,使用协作组需要包含对应头文件并使用命名空间:

#include "cooperative_groups.h"

using namespace cooperative_groups;

协作组编程模型中最基本的类型是线程组thread_group,它的成员如下:

//同步组内所有线程
void sync(); //返回组内总的线程数目
unsigned int size(); //返回当前调用该函数的线程在组内的标号(从0开始计数)
unsigned int thread_rank(); //如果定义的组违反了任何CUDA限制则返回false,否则返回true
bool is_valid();

thread_block派生自thread_group,并提供了额外的函数:

//返回当前调用该函数的线程的线程块标号,等价于blockIdx
dim3 group_index(); //返回当前调用该函数的线程的线程标号,等价于threadIdx
dim3 thread_index();

可以用如下方式定义并初始化一个thread_block对象:

thread_block tb = this_thread_block();

其中this_thread_block()相当于一个线程块类型的常量,这样定义的tb就代表当前线程块,只不过这里把它包装成了一个类型。例如,tb.sync()完全等价于__syncthreads()tb.group_index()完全等价于blockIdxtb.thread_index()完全等价于threadIdx

可以用函数tiled_partition将一个线程块划分成若干片(tile),每片构成一个新的线程组,目前仅可将片的大小设置为2、4、8、16、32中的一个。线程组也可以被分割为更细的线程组。

thread_group g32 = tiled_partition(this_thread_block(), 32);
thread_group g4 = tiled_partition(g32, 4);

如果线程组的大小在编译期就已知,那么就可以使用模板化的版本进行定义,这样可能会更高效。

thread_block_tile<32> g32 = tiled_partition<32>(this_thread_block());

对于用模板定义的线程块片(thread block tile),还可以使用如下函数(类似线程束内的基本函数):

int any(int predicate);
int all(int predicate);
unsigned int ballot(int predicate);
T shfl(T var, int srcLane);
T shfl_down(T var, unsigned int delta);
T shfl_up(T var, unsigned int delta);
T shfl_xor(T var, unsigned int laneMask);

与对应的线程束内基本函数相比,上述线程块片的函数主要有两点不同:①、少了代表掩码的参数,因为线程组内所有线程都必须参与函数的计算;②、洗牌函数少了最后一个代表宽度的参数,因为宽度就等于线程块片的大小,即模板参数。

4. 原子函数

原子函数对它第一个参数指向的数据进行一次“读-改-写”的原子操作,第一个参数可以指向全局内存,也可以指向共享内存。原子操作是一个线程一个线程轮流做的,但没有明确的次序,另外,原子函数没有同步功能。所有原子函数都是__device__函数,只能在核函数中使用。

下表列出了所有原子函数的原型,address所指变量的值在执行原子函数前为old,执行原子函数后为new。对于每个原子函数,返回值都是old

运算 功能 函数原型
加法 new = old + val T atomicAdd(T* address, T val);
减法 new = old - val T atomicSub(T* address, T val);
自增 new = (old >= val) ? 0 : (old + 1) T atomicInc(T* address, T val);
自减 new = ((old == 0) || (old > val)) ? val : (old - 1) T atomicDec(T* address, T val);
最小值 new = (old < val) : old : val T atomicMin(T* address, T val);
最大值 new = (old > val) : old : val T atomicMax(T* address, T val);
交换 new = val T atomicExch(T* address, T val);
比较-交换(compare and swap) new = (old == compare) ? val : old T atomicCAS(T* address, T compare, T val);
按位与 new = old & val T atomicAnd(T* address, T val);
按位或 new = old | val T atomicOr(T* address, T val);
按位异或 new = old ^ val T atomicXor(T* address, T val);

上面所列的函数中,我们用T表示相关变量的数据类型。各个原子函数对数据类型的支持情况见下表。注:atomicAdddouble__half2的支持始于Pascal架构,对__half的支持始于Volta架构。

原子函数 int unsigned unsigned long long float double __half2 __half
atomicAdd yes yes yes yes yes yes yes
atomicSub yes yes no no no no no
atomicInc no yes no no no no no
atomicDec no yes no no no no no
atomicMin yes yes yes no no no no
atomicMax yes yes yes no no no no
atomicExch yes yes yes yes no no no
atomicCAS yes yes yes no no no no
atomicAnd yes yes yes no no no no
atomicOr yes yes yes no no no no
atomicXor yes yes yes no no no no

《CUDA编程:基础与实践》读书笔记(3):同步、协作组、原子函数的更多相关文章

  1. 《Python基础教程》 读书笔记 第六章 抽象 函数 参数

    6.1创建函数 函数是可以调用(可能包含参数,也就是放在圆括号中的值),它执行某种行为并且返回一个值.一般来说,内建的callable函数可以用来判断函数是否可调用: >>> x=1 ...

  2. 《Java并发编程的艺术》读书笔记:二、Java并发机制的底层实现原理

    二.Java并发机制底层实现原理 这里是我的<Java并发编程的艺术>读书笔记的第二篇,对前文有兴趣的朋友可以去这里看第一篇:一.并发编程的目的与挑战 有兴趣讨论的朋友可以给我留言! 1. ...

  3. 《jQuery基础教程》读书笔记

    最近在看<jQuery基础教程>这本书,做了点读书笔记以备回顾,不定期更新. 第一章第二章比较基础,就此略过了... 第三章 事件 jQuery中$(document).ready()与j ...

  4. 《Java并发编程的艺术》读书笔记:一、并发编程的目的与挑战

    发现自己有很多读书笔记了,但是一直都是自己闷头背,没有输出,突然想起还有博客圆这么个好平台给我留着位置,可不能荒废了. 此文读的书是<Jvava并发编程的艺术>,方腾飞等著,非常经典的一本 ...

  5. cuda编程基础

    转自: http://blog.csdn.net/augusdi/article/details/12529247 CUDA编程模型 CUDA编程模型将CPU作为主机,GPU作为协处理器(co-pro ...

  6. MDX Step by Step 读书笔记(七) - Performing Aggregation 聚合函数之 Max, Min, Count , DistinctCount 以及其它 TopCount, Generate

    MDX 中最大值和最小值 MDX 中最大值和最小值函数的语法和之前看到的 Sum 以及 Aggregate 等聚合函数基本上是一样的: Max( {Set} [, Expression]) Min( ...

  7. Java并发编程实践读书笔记(2)多线程基础组件

    同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...

  8. Java并发编程实践读书笔记(5) 线程池的使用

    Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...

  9. Java并发编程实践(读书笔记) 任务执行(未完)

    任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元.   任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任 ...

  10. Java并发编程实践读书笔记(1)线程安全性和对象的共享

    2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...

随机推荐

  1. 3.11 Linux删除空目录(rmdir命令)

    和 mkdir 命令(创建空目录)恰好相反,rmdir(remove empty directories 的缩写)命令用于删除空目录,此命令的基本格式为: [root@localhost ~]# rm ...

  2. 利用 Screen 保持 VSCode 连接远程任务持续运行

    在 Linux 上使用 screen 是一种保持进程持续运行的便捷方式,即使用户断开 SSH 连接,进程也不会中断. 我在使用VSCode连接AutoDL时,不知道如何能够使进程保持运行,后查阅资料可 ...

  3. 这可能是最好的Spring教程!即便无基础也能看懂的入门Spring,仍在持续更新。

    开启这样一个系列的原因 这一段时间都在学spring,但是在学习的过程中一直都很难找到一个通俗易懂,又带了学习体系的文章教程,很多地方都不懂,需要自己去慢慢查询和理解,感觉学起来很耗时,所以我自己就像 ...

  4. 【鸣潮,原神PC端启动器】仿二次元手游PC端游戏启动器,以鸣潮为例。

    二游GAMELanucher启动器 1.前言 许多二次元手游(原神,鸣潮,少女前线)的PC端启动器都是使用Qt做的,正好最近正在玩鸣潮,心血来潮,便仿鸣潮启动器,从头写一个.先下载一个官方版的PC启动 ...

  5. API13Bate版来了DevEco已更新快来看新功能吧

    HarmonyOS 5.0.1 Beta3,是HarmonyOS开发套件基于API 13正式发布的首个Beta版本.该版本在OS能力上主要增强了C API的相关能力,多个特性补充了C API供开发者使 ...

  6. canvas绘制--圆角多边形

    context.arcTo() arcTo() 方法在画布上创建介于两个切线之间的弧/曲线. JavaScript 语法: context.arcTo(x1,y1,x2,y2,r); 参数描述 参数 ...

  7. java4~6次大作业全面总结

    一:前言: 知识点总结: 面向对象设计: 智能家居强电电路模拟系统:设计了多种控制设备(开关.分档调速器.连续调速器)和受控设备(灯.风扇)的类,并通过继承和多态实现设备的特有行为. 答题判题程序:设 ...

  8. PHP5.2-5.6不同版本新特性

    温故而知新, 时常复习下之前的东西 还是会有一些收获 本文目录:PHP5.2 以前:autoload, PDO 和 MySQLi, 类型约束PHP5.2:JSON 支持PHP5.3:弃用的功能,匿名函 ...

  9. uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)

    自研uni-app+vue3+uv-ui跨三端仿同程/携程酒店预订系统Uni-Vue3-WeTrip. uniapp-vue3-wetrip原创基于vite5+uniapp+pinia2+uni-ui ...

  10. .NET 中的中间件(Middleware)

    ASP.NET Core 中间件 什么是中间件(Middleware)? 中间件是组装到应用程序管道中以处理请求和响应的软件. 每个组件: 选择是否将请求传递给管道中的下一个组件. 可以在调用管道中的 ...