【CUDA 基础】3.4 避免分支分化
—
title: 【CUDA 基础】3.4 避免分支分化
categories:
- CUDA
- Freshman
tags:
- 规约问题
- 分支分化
toc: true
date: 2018-04-17 23:32:55
Abstract: 介绍规约问题中的分支分化问题
Keywords: 规约问题,分支分化 此篇有些结果和参考书中结果相反,需要更深入的技术才能解决
开篇废话
我坚持写博客是因为我上次最困惑最难过的那段时间通过写博客改变了我的非常不好的情况,所以我认为写些东西梳理自己的思路能够改变我的生活,所以我会一直坚持,学习的内容是没有止境的,所以博客也可以写很多。
写博客为了收入我之前也想过,最后放弃了,因为如果你的目的就是挣钱,有写博客这大把时间还不如出去跑个滴滴或者送个外卖来得快,所以我把之前有的捐赠部分都取消掉了,我并不否定那些博客写的质量非常高的人因此有收入,做得好,帮助到人了就可以获得收入,但是为了收入去帮助人,那叫服务。所以我后面可能会挂一个小广告,但是绝对不会因为广告搞得博客非常凌乱,而且收入应该只用作服务器和域名费用,仅此而已,我个人对于做事非常看重目的,我的目的是分享知识,并不是收入,收入只是附加的。
本文介绍一个并行计算中最常见的典型情况,并行分化(线程束分化的等价问题),以及规约问题,以及其初步优化。
并行规约问题
在串行编程中,我们最最最常见的一个问题就是一组特别多数字通过计算变成一个数字,比如加法,也就是求这一组数据的和,或者乘法,这种计算当有如下特点的时候,我们可以用并行归约的方法处理他们:
- 结合性
 - 交换性
 
对应的加法或者乘法就是交换律和结合律,在我们的数学分析系列已经详细的介绍了加法和乘法的结合律和交换律的证明。所以对于所有有这两个性质的计算,都可以使用归约式计算。
为什么叫归约,归约是一种常见的计算方式(串并行都可以),一开始我听到这个名字的时候应该是在两年前了,感觉很迷惑,后来发现,归约的归有递归的意思,约就是减少,这样就很明显了,每次迭代计算方式都是相同的(归),从一组多个数据最后得到一个数(约)。
归约的方式基本包括如下几个步骤:
- 将输入向量划分到更小的数据块中
 - 用一个线程计算一个数据块的部分和
 - 对每个数据块的部分和再求和得到最终的结果。
 
数据分块保证我们可以用一个线程块来处理一个数据块。
一个线程处理更小的块,所以一个线程块可以处理一个较大的块,然后多个块完成整个数据集的处理。
最后将所有线程块得到的结果相加,就是结果,这一步一般在cpu上完成。
归约问题最常见的加法计算是把向量的数据分成对,然后用不同线程计算每一对元素,得到的结果作为输入继续分成对,迭代的进行,直到最后一个元素。
成对的划分常见的方法有以下两种:
- 相邻配对:元素与他们相邻的元素配对

 - 交错配对:元素与一定距离的元素配对

 
图中将两种方式表现的很清楚了,我们可以用代码实现以下。
首先是cpu版本实现交错配对归约计算的代码:
int recursiveReduce(int *data, int const size)
{
	// terminate check
	if (size == 1)
        return data[0];
	// renew the stride
	int const stride = size / 2;
	if (size % 2 == 1)
	{
		for (int i = 0; i < stride; i++)
		{
			data[i] += data[i + stride];
		}
		data[0] += data[size - 1];
	}
	else
	{
		for (int i = 0; i < stride; i++)
		{
			data[i] += data[i + stride];
		}
	}
	// call
	return recursiveReduce(data, stride);
}
和书上的代码有些不同,因为书上的代码没有考虑数组长度非2的整数幂次的结果。所以我加了一个处理奇数数组最后一个无人配对的元素的处理。
这个加法运算可以改成任何满足结合律和交换律的计算。比如乘法,求最大值等。
下面我们就来通过不同的配对方式,不同的数据组织来看CUDA的执行效率。
并行规约中的分化
线程束分化已经明确说明了,有判断条件的地方就会产生分支,比如if 和 for这类关键词。
如下图所表示的那样,我们对相邻元素配对进行内核实现的流程描述:

根据上一小节介绍:
第一步:是把这个一个数组分块,每一块只包含部分数据,如上图那样(图中数据较少,但是我们假设一块上只有这么多。),我们假定这是线程块的全部数据
第二步:就是每个线程要做的事,橙色圆圈就是每个线程做的操作,可见线程threadIdx.x=0 的线程进行了三次计算,奇数线程一致在陪跑,没做过任何计算,但是根据3.2中介绍,这些线程虽然什么都不干,但是不可以执行别的指令,4号线程做了两步计算,2号和6号只做了一次计算。
第三步:将所有块得到的结果相加,就是最终结果
这个计算划分就是最简单的并行规约算法,完全符合上面我们提到的三步走的套路
值得注意的是,我们每次进行一轮计算(黄色框,这些操作同时并行)的时候,部分全局内存要进行一次修改,但只有部分被替换,而不被替换的,也不会在后面被使用到,如蓝色框里标注的内存,就被读了一次,后面就完全没有人管了。
我们现在把我们的内核代码贴出来
__global__ void reduceNeighbored(int * g_idata,int * g_odata,unsigned int n)
{
	//set thread ID
	unsigned int tid = threadIdx.x;
	//boundary check
	if (tid >= n) return;
	//convert global data pointer to the
	int *idata = g_idata + blockIdx.x*blockDim.x;
	//in-place reduction in global memory
	for (int stride = 1; stride < blockDim.x; stride *= 2)
	{
		if ((tid % (2 * stride)) == 0)
		{
			idata[tid] += idata[tid + stride];
		}
		//synchronize within block
		__syncthreads();
	}
	//write result for this block to global mem
	if (tid == 0)
		g_odata[blockIdx.x] = idata[0];
}
这里面唯一要注意的地方就是同步指令
__syncthreads();
原因还是能从图上找到,我们的每一轮操作都是并行的,但是不保证所有线程能同时执行完毕,所以需要等待,执行的快的等待慢的,这样就能避免块内的线程竞争内存了。
被操作的两个对象之间的距离叫做跨度,也就是变量stride,
完整的执行逻辑如下,

注意主机端和设备端的分界,注意设备端的数据分块。
完整的可执行代码Github:https://github.com/Tony-Tan/CUDA_Freshman
这里把主函数贴出来,但注意里面包含后面的核函数执行部分,所以想要运行还是去github上拉一下吧,顺便点个star
int main(int argc,char** argv)
{
	.........
    int size = 1 << 24;
    .........
	dim3 block(blocksize, 1);
	dim3 grid((size - 1) / block.x + 1, 1);
    .........
	//cpu reduction
	int cpu_sum = 0;
	iStart = cpuSecond();
	for (int i = 0; i < size; i++)
		cpu_sum += tmp[i];
	printf("cpu sum:%d \n", cpu_sum);
	iElaps = cpuSecond() - iStart;
	printf("cpu reduce                 elapsed %lf ms cpu_sum: %d\n", iElaps, cpu_sum);
	//kernel 1:reduceNeighbored
	CHECK(cudaMemcpy(idata_dev, idata_host, bytes, cudaMemcpyHostToDevice));
	CHECK(cudaDeviceSynchronize());
	iStart = cpuSecond();
	warmup <<<grid, block >>>(idata_dev, odata_dev, size);
	cudaDeviceSynchronize();
	iElaps = cpuSecond() - iStart;
	cudaMemcpy(odata_host, odata_dev, grid.x * sizeof(int), cudaMemcpyDeviceToHost);
	gpu_sum = 0;
	for (int i = 0; i < grid.x; i++)
		gpu_sum += odata_host[i];
	printf("gpu warmup                 elapsed %lf ms gpu_sum: %d<<<grid %d block %d>>>\n",
		iElaps, gpu_sum, grid.x, block.x);
	//kernel 1:reduceNeighbored
	CHECK(cudaMemcpy(idata_dev, idata_host, bytes, cudaMemcpyHostToDevice));
	CHECK(cudaDeviceSynchronize());
	iStart = cpuSecond();
	reduceNeighbored << <grid, block >> >(idata_dev, odata_dev, size);
	cudaDeviceSynchronize();
	iElaps = cpuSecond() - iStart;
	cudaMemcpy(odata_host, odata_dev, grid.x * sizeof(int), cudaMemcpyDeviceToHost);
	gpu_sum = 0;
	for (int i = 0; i < grid.x; i++)
		gpu_sum += odata_host[i];
	printf("gpu reduceNeighbored       elapsed %lf ms gpu_sum: %d<<<grid %d block %d>>>\n",
		iElaps, gpu_sum, grid.x, block.x);
	//kernel 2:reduceNeighboredLess
	CHECK(cudaMemcpy(idata_dev, idata_host, bytes, cudaMemcpyHostToDevice));
	CHECK(cudaDeviceSynchronize());
	iStart = cpuSecond();
	reduceNeighboredLess <<<grid, block>>>(idata_dev, odata_dev, size);
	cudaDeviceSynchronize();
	iElaps = cpuSecond() - iStart;
	cudaMemcpy(odata_host, odata_dev, grid.x * sizeof(int), cudaMemcpyDeviceToHost);
	gpu_sum = 0;
	for (int i = 0; i < grid.x; i++)
		gpu_sum += odata_host[i];
	printf("gpu reduceNeighboredLess   elapsed %lf ms gpu_sum: %d<<<grid %d block %d>>>\n",
		iElaps, gpu_sum, grid.x, block.x);
	//kernel 3:reduceInterleaved
	CHECK(cudaMemcpy(idata_dev, idata_host, bytes, cudaMemcpyHostToDevice));
	CHECK(cudaDeviceSynchronize());
	iStart = cpuSecond();
	reduceInterleaved << <grid, block >> >(idata_dev, odata_dev, size);
	cudaDeviceSynchronize();
	iElaps = cpuSecond() - iStart;
	cudaMemcpy(odata_host, odata_dev, grid.x * sizeof(int), cudaMemcpyDeviceToHost);
	gpu_sum = 0;
	for (int i = 0; i < grid.x; i++)
		gpu_sum += odata_host[i];
	printf("gpu reduceInterleaved      elapsed %lf ms gpu_sum: %d<<<grid %d block %d>>>\n",
		iElaps, gpu_sum, grid.x, block.x);
	// free host memory
	.....
}
代码太长不美观,删减一下,只留下了内核执行部分,可见,主函数只有最后一个循环求和的过程是要注意别忘了的,其他都是常规操作
还有一点,需要注意实际任务中数组不可能每次都是2的整数幂,如果不是2的整数幂需要确定数组边界。

上图就是执行结果,为啥有那么多,因为我把下面两个经过优化的也装进去了,黄色框框里是我们上面这段代码执行结果和时间,warmup 是为了启动gpu防止首次启动计算时gpu的启动过程耽误时间,影响效率测试,warmup的代码就是reducneighbored的代码,可见还是有微弱的差别的。
改善并行规约的分化
完整内容参考https://face2ai.com/CUDA-F-3-4-避免分支分化/
【CUDA 基础】3.4 避免分支分化的更多相关文章
- 【CUDA 基础】5.3 减少全局内存访问
		
title: [CUDA 基础]5.3 减少全局内存访问 categories: - CUDA - Freshman tags: - 共享内存 - 归约 toc: true date: 2018-06 ...
 - 【CUDA 基础】3.2 理解线程束执行的本质(Part I)
		
title: [CUDA 基础]3.2 理解线程束执行的本质(Part I) categories: CUDA Freshman tags: 线程束分化 CUDA分支 toc: true date: ...
 - CUDA基础介绍
		
一.GPU简介 1985年8月20日ATi公司成立,同年10月ATi使用ASIC技术开发出了第一款图形芯片和图形卡,1992年4月ATi发布了Mach32图形卡集成了图形加速功能,1998年4月ATi ...
 - 【CUDA 基础】3.5 展开循环
		
title: [CUDA 基础]3.5 展开循环 categories: - CUDA - Freshman tags: - 展开归约 - 归约 - 模板函数 toc: true date: 2018 ...
 - 【CUDA 基础】3.1 CUDA执行模型概述
		
title: [CUDA 基础]3.1 CUDA执行模型概述 categories: CUDA Freshman tags: CUDA SM SIMT SIMD Fermi Kepler toc: t ...
 - 【CUDA 基础】6.5 流回调
		
title: [CUDA 基础]6.5 流回调 categories: - CUDA - Freshman tags: - 流回调 toc: true date: 2018-06-20 21:56:1 ...
 - 【CUDA 基础】6.3 重叠内和执行和数据传输
		
title: [CUDA 基础]6.3 重叠内和执行和数据传输 categories: - CUDA - Freshman tags: - 深度优先 - 广度优先 toc: true date: 20 ...
 - 【CUDA 基础】6.1 流和事件概述
		
title: [CUDA 基础]6.1 流和事件概述 categories: - CUDA - Freshman tags: - 流 - 事件 toc: true date: 2018-06-10 2 ...
 - 【CUDA 基础】6.2 并发内核执行
		
title: [CUDA 基础]6.2 并发内核执行 categories: - CUDA - Freshman tags: - 流 - 事件 - 深度优先 - 广度优先 - 硬件工作队列 - 默认流 ...
 
随机推荐
- python 安装PostgreSQL 模块:psycopg2
			
官方资料:http://www.psycopg.org/psycopg/docs/ 安装: yum -y install python-psycopg2 (安装的版本可能是2.0) pip insta ...
 - 怎样理解 Vue 组件中 data 必须为函数 ?
			
组件意在 复用 , 若为 对象, 则会相互干扰. 且 Vue 不允许此事发生, 规定必须为函数, 否则报错. 原理如下 对象 // 模拟创建组件 var Component= function() { ...
 - 怎样使用 v-on 指令?
			
1. Vue 中的 v-on 指令用于绑定 dom 事件 的监听函数. 下面代码实现的是 点击更改文字颜色 的功能. <!DOCTYPE html> <html lang=" ...
 - DDOS攻击脚本
			
import sysimport osimport timeimport socketimport random#Code Timefrom datetime import datetimenow = ...
 - Oracle 11.2.0.1 ADG环境MRP进程遭遇ORA
			
环境:Linux + Oracle 11.2.0.1 ADG现象:发现备库没有应用日志 1. 数据库查询备库目前状态发现备库目前没有应用日志,apply lag已经显示备库有3天21小时多没有应用日志 ...
 - 第十章、typing模块
			
目录 第十章.typing模块 一.说明 二.typing模块的作用 三.使用typing模块 四.typing常用类型 第十章.typing模块 一.说明 二.typing模块的作用 类型检查,防止 ...
 - Windows环境中编译opencv3.0同时加入OPENCV_contrib库及解决遇到相关问题[contrib 必须要3.1以上的opencv才支持了]
			
更新:现在contrib库必须要opencv3.1以上才能支持编译通过了. 方法和步骤还是和本篇文章一样. ############################################## ...
 - XML基础介绍【二】
			
XML基础介绍[二] 1.schema约束dtd语法: <!ELEMENT 元素名称 约束>schema符合xml的语法,xml语句.一个xml中可以有多个schema,多个schema使 ...
 - 十一,k8s集群访问控制之ServicAccount
			
目录 认证安全 连接Api-Server的两类账号 ServiceAccount 创建 使用admin 的SA 测试 URL访问kubernetes资源 APIserver客户端定义的配置文件 kub ...
 - 8.8.ZooKeeper 原理和选举机制
			
1.ZooKeeper原理 Zookeeper虽然在配置文件中并没有指定master和slave但是,zookeeper工作时,是有一个节点为leader,其他则为follower,Leader是通 ...