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. awk 处理 Git 提交信息生成 Release Note

    发布软件时通常都会写 Release Note,但每次从头手打也有点累,可以考虑从 Git 的提交历史中自动生成. Git 提交信息一般是三段式结构,段落之间使用空行隔开: <subject&g ...

  2. buck电路 & boost电路

    buck电路 buck电路是直流的降压电路,我们下面给大家讲下,如何把12V的直流电压降压成5V的直流电压 1.buck电路拓扑:12V----->5V 2.降压原理 a.开关闭合,电流走向 电 ...

  3. Kubernetes上安装nacos

    k8s配置 --- apiVersion: apps/v1 kind: Deployment metadata: name: nacos namespace: com spec: selector: ...

  4. 2024 Navicat Premium 16+17安装教程(附激活方法)

    Navicat Premium,作为一款功能全面的集成数据库管理工具,无缝支持多样化的数据库类型,为用户带来前所未有的高效与便捷管理体验.它不仅涵盖了连接管理.数据导入导出.同步迁移.备份恢复等核心功 ...

  5. Galera_Cluster_Mysql部署

    前言 先来了解下它的身世,Galera Cluster是Codership公司开发的一套免费开源的高可用方案 官网为http://galeracluster.com.Galera Cluster即为安 ...

  6. CUDA编程学习 (1)——CUDA C介绍

    1. 内存分配和数据移动 API 函数 CUDA编程模型是一个异构模型,需要CPU和GPU协同工作.在CUDA中,host 和 devic e 是两个重要的概念,我们用host指代CPU及其内存,而用 ...

  7. delphi12 Android Edit SDK安装

    安装 delphi 12.1 后,编译 FMX Android 程序失败! 查找原因,SDK配置全是叹号! 之前用过SDK Manager.exe,直接打开即可以选择下载,但现在发现没有了,查找资料如 ...

  8. swoole的安装

    因为换了一台工作电脑 需要重新安装各种环境,这里简单记录一下swoole的安装步骤. 首先去下载它的git仓库: $ git clone https://gitee.com/swoole/swoole ...

  9. 盘点Vue3 watch的一些关键时刻能够大显身手的功能

    前言 watch这个API大家应该都不陌生,在Vue3版本中给watch增加不少有用的功能,比如deep选项支持传入数字.pause.resume.stop方法.once选项.onCleanup函数. ...

  10. vue中获取v-for循环出来的元素的相对于父级的最左边的距离