【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: - 流 - 事件 - 深度优先 - 广度优先 - 硬件工作队列 - 默认流 ...
随机推荐
- 刨根究底字符编码之十——Unicode字符集的编码方式以及码点、码元
Unicode字符集的编码方式以及码点.码元 一.字符编码方式CEF的选择 1. 由于Unicode字符集非常大,有些字符的编号(码点值)需要两个或两个以上字节来表示,而要对这样的编号进行编码,也必须 ...
- Jmeter安装及配置(含JDK安装)
(一)java环境配置1.官网下载64位的jdkhttp://www.oracle.com/technetwork/java/javase/downloads/index.html 2.环境变量设置变 ...
- go intall的使用
1.首先GOPATH路径指向src的上级目录 2.设置GOBIN路径指向bin目录 3.查看环境配置 4.go install 在src目录下 5.完成 6.pkg ide编译运行一下自动生成
- Hyperledger Fabric-sdk-java
Hyperledger Fabric-sdk-java 2018年04月18日 23:36:02 l_ricardo 阅读数 975更多 分类专栏: 区块链 java 版权声明:本文为博主原创文章 ...
- Java EE javax.servlet ServletContainerInitializer接口
ServletContainerInitializer接口 public interface ServletContainerInitializer 一.介绍 该接口,允许在 web 应用程序的启动阶 ...
- oracle建表详细信息
一张用户表 -- Create table create table OA_DM.DM_GY_USER ( ), username ) not null, loginname ) not null, ...
- Css文字效果
1.文字样式:font-family 字体:sans-serif(字体醒目):serif(笔画的开始和结束有额外的修饰部分,易读性较强) *页面设计最好有多个备选字体 2.文字大小:font-size ...
- C++ STL用法总结(持续更新)
Vector 动态数组 https://www.cnblogs.com/zhonghuasong/p/5975979.html lower_bound&&upper_bound htt ...
- Const指针 、 指向const的指针 、引用、指针
1. const指针和 指向const的指针 指向const的指针: 不允许通过指针来改变其指向的const值 const double *cptr *cptr = 42; // error! 指针 ...
- mintUI和mUI
mintUI 安装: npm install mint-ui -S 引入: // 按需引入部分组件 import { Cell, Checklist } from 'mint-ui'; Vue.com ...