GPU编程自学5 —— 线程协作
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题。这里主要记录自己的GPU自学历程。
目录
- 《GPU编程自学1 —— 引言》
- 《GPU编程自学2 —— CUDA环境配置》
- 《GPU编程自学3 —— CUDA程序初探》
- 《GPU编程自学4 —— CUDA核函数运行参数》
- 《GPU编程自学5 —— 线程协作》
- 《GPU编程自学6 —— 函数与变量类型限定符》
- 《GPU编程自学7 —— 常量内存与事件》
五、 线程协作
5.1 并行程序块的分解
首先回顾我们之前实现的矢量相加程序:
// 核函数:每个线程负责一个分量的加法
__global__ void addKernel(int *c, const int *a, const int *b)
{
int i = threadIdx.x; // 获取线程ID
c[i] = a[i] + b[i];
}
// 运行核函数,运行设置为1个block,每个block中size个线程
addKernel << <1, size >> >(dev_c, dev_a, dev_b);
通过前面小节,我们知道一个Block中的可开辟的线程数量是有限的(不超过1024)。
如果矢量特别长,上面的操作是会出现问题的。于是我们可以采用多个线程块(Block)来解决线程不足的问题。 假如我们设定每个线程块包含128个线程,则需要的线程块的数量为 size / 128。 为了避免不能整除带来的问题,我们可以稍微多开一点 (size + 127) / 128,但需要增加判断条件来避免越界。
// 核函数:每个线程负责一个分量的加法
__global__ void addKernel(int *c, const int *a, const int *b, const int size)
{
int i = blockIdx.x * blockDim.x + threadIdx.x; // 获取线程ID
if (i < size)
c[i] = a[i] + b[i];
}
// 运行核函数,运行设置为多个block,每个block中128个线程
addKernel <<<(size + 127) / 128, 128 >>>(dev_c, dev_a, dev_b, size);
通过前面小节,我们同时也知道一个Grid中可开辟的Block数量也是有限的。
如果数据量大于 Block_num * Thread_num,那么我们就无法为每个分量单独分配一个线程了。 不过,一个简单的解决办法就是在核函数中增加循环。
// 核函数:每个线程负责多个分量的加法
__global__ void addKernel(int *c, const int *a, const int *b, const int size)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
while (i < size)
{
c[i] = a[i] + b[i];
// 偏移分量等于一个Grid中包含的线程数量
i += blockDim.x * gridDim.x;
}
}
// 运行核函数,运行设置为1个Grid包含128个block,每个block包含128个线程
// 其中已经假设 size > 128*128
addKernel <<<128, 128 >>>(dev_c, dev_a, dev_b, size);
5.2 共享内存与同步
上面提到线程块的分解似乎是为了增加可用的线程数量,但这种说法并不靠谱,因为这完全可以由CUDA在幕后全权控制。 其实,分解线程块的重要原因是因为内存。
在“4.3 内存结构”中我们已经知道,同一个Block中的线程可以访问一块共享内存。由于共享内存缓冲区驻留在物理GPU上,而不是GPU之外的系统内存上,因此访问共享内存的延迟要远远低于访问普通缓冲区的延迟。
不同Block之间存在隔离,如果我们需要不同线程之间进行通信,那么还需要考虑线程同步的问题。比如线程A将某个数值写入内存,然后线程B会对该数值进行一些操作,那么显然必须等A完成之后B才可以操作,如果没有同步,程序将会因进入“竞态条件”而产生意想不到的错误。
接下来我们通过一个矢量点积的例子来说明上述问题。
矢量点积的定义如下:
\((x_1,x_2,x_3,x_4)\cdot (y_1,y_2,y_3,y_4)=x_1y_1+x_2y_2+x_3y_3+x_4y_4\)
由上面的定义来看,点积的实现可以分为两步:
- (1)计算每个分量的乘积,并暂存该结果;
- (2)将所有临时结果加和。
我们先来简单实现下第一步:
// 定义我们的矢量长度
const int N = 64 * 256;
// 定义每个Block中包含的Thread数量
const int threadsPerBlock = 256;
// 定义每个Grid中包含的Block数量, 这里32 < 64, 是为了模拟线程数量不足的情况
const int blocksPerGrid = 32;
__global__ void dot( float *a, float *b, float *c )
{
// 声明共享内存用于存储临时乘积结果,内存大小为1个Block中的线程数量
// PS. 每个Block都相当于有一份程序副本,因此相当于每个Block都有这样的一份共享内存
__shared__ float cache[threadsPerBlock];
// 线程索引
int tid = threadIdx.x + blockIdx.x * blockDim.x;
// 一个Block中的线程索引
int cacheIndex = threadIdx.x;
// 计算分量乘积,同时处理线程不足的问题
float temp = 0;
while (tid < N) {
temp += a[tid] * b[tid];
tid += blockDim.x * gridDim.x;
}
// 存储临时乘积结果
cache[cacheIndex] = temp;
}
执行完上面的部分,我们剩下的就是把cache中的值相加求和。 但是,我们必须要保证所有乘积都已经计算完成,才能去计算求和。 命令如下:
// 对线程块中的所有线程进行同步
// 线程块中的所有线程都执行完前面的代码后才会继续往后执行
__syncthreads();
求和最直接的方式就是循环累加,此时复杂度与数组长度成正比。不过我们可以再用一种更加高效的方法,其复杂度与数组长度的log成正比:将值两两合并,于是数据量减小一半,再重复两两合并直至全部计算完成。代码如下:
// 合并算法要求长度为2的指数倍
int i = blockDim.x/2;
while (i != 0)
{
if (cacheIndex < i)
cache[cacheIndex] += cache[cacheIndex + i];
__syncthreads();
i /= 2;
}
// 最后将一个Block的求和结果进行保存
if (cacheIndex == 0)
c[blockIdx.x] = cache[0];
下面给出完整的代码(简单起见,不再做错误检查):
#include <iostream>
#include "cuda_runtime.h"
//定义矢量长度
const int N = 64 * 256;
// 定义每个Block中包含的Thread数量
const int threadsPerBlock = 256;
// 定义每个Grid中包含的Block数量, 这里32 < 64, 是为了模拟线程数量不足的情况
const int blocksPerGrid = 32;
// 核函数:矢量点积
__global__ void dot(float* a, float* b, float* c)
{
// 声明共享内存用于存储临时乘积结果,内存大小为1个Block中的线程数量
// PS. 每个Block都相当于有一份程序副本,因此相当于每个Block都有这样的一份共享内存
__shared__ float cache[threadsPerBlock];
// 线程索引
int tid = blockIdx.x * blockDim.x + threadIdx.x;
// 一个Block中的线程索引
int cacheIndex = threadIdx.x;
// 计算分量乘积,同时处理线程不足的问题
float temp = 0.0f;
while (tid < N)
{
temp += a[tid] * b[tid];
tid += gridDim.x * blockDim.x;
}
// 存储临时乘积结果
cache[cacheIndex] = temp;
// 对线程块中的所有线程进行同步
// 线程块中的所有线程都执行完前面的代码后才会继续往后执行
__syncthreads();
// 合并算法要求长度为2的指数倍
int i = threadsPerBlock / 2;
while (i != 0)
{
if (cacheIndex < i)
cache[cacheIndex] += cache[cacheIndex + i];
__syncthreads();
i /= 2;
}
if (cacheIndex == 0)
c[blockIdx.x] = cache[0];
}
int main()
{
// 在主机端创建数组
float a[N];
float b[N];
float c[threadsPerBlock];
for (size_t i = 0; i < N; i++)
{
a[i] = 1.f;
b[i] = 1.f;
}
// 申请GPU内存
float* dev_a = nullptr;
float* dev_b = nullptr;
float* dev_c = nullptr;
cudaMalloc((void**)&dev_a, N * sizeof(float));
cudaMalloc((void**)&dev_b, N * sizeof(float));
cudaMalloc((void**)&dev_c, blocksPerGrid * sizeof(float));
//将数据从主机copy进GPU
cudaMemcpy(dev_a, a, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, b, N * sizeof(float), cudaMemcpyHostToDevice);
//进行点积计算
dot<<<32, 256>>>(dev_a, dev_b, dev_c);
//将计算结果copy回主机
cudaMemcpy(c, dev_c, blocksPerGrid * sizeof(float), cudaMemcpyDeviceToHost);
//将每个block的结果进行累加
for (size_t i = 1; i < blocksPerGrid; i++)
c[0] += c[i];
// 输出结果
std::cout << "The ground truth is 16384, our answer is " << c[0] << std::endl;
//释放内存
cudaFree(dev_a); cudaFree(dev_b); cudaFree(dev_c);
system("pause");
return 0;
}
参考资料:
- 《CUDA by Example: An Introduction to General-Purpose GPU Programming》 中文名《GPU高性能编程CUDA实战》
- 深入理解CUDA点积运算 https://segmentfault.com/a/1190000007519944
GPU编程自学5 —— 线程协作的更多相关文章
- GPU编程自学7 —— 常量内存与事件
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学6 —— 函数与变量类型限定符
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学4 —— CUDA核函数运行参数
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学3 —— CUDA程序初探
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学2 —— CUDA环境配置
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学1 —— 引言
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- CUDA线程协作之共享存储器“__shared__”&&“__syncthreads()”
在GPU并行编程中,一般情况下,各个处理器都需要了解其他处理器的执行状态,在各个并行副本之间进行通信和协作,这涉及到不同线程间的通信机制和并行执行线程的同步机制. 共享内存"__share_ ...
- GPU 编程入门到精通(五)之 GPU 程序优化进阶
博主因为工作其中的须要,開始学习 GPU 上面的编程,主要涉及到的是基于 GPU 的深度学习方面的知识.鉴于之前没有接触过 GPU 编程.因此在这里特地学习一下 GPU 上面的编程. 有志同道合的小伙 ...
- GPU编程--宏观理解篇(1)
GPU编程与CPU编程最大的不同可以概括为以下两点: "The same program is executed on many data elements in parallel" ...
随机推荐
- nfs挂载
安装: yum install nfs-utils rpcbind 配置共享目录:vim /etc/exports /xxx/cloudcms *(insecure,rw,async,no_root_ ...
- 照着官网来安装openstack pike之nova安装
nova组件安装分为控制节点和计算节点,还是先从控制节点安装 1.前提条件,数据库为nova创建库和账户密码来连接数据库 # mysql -u root -p MariaDB [(none)]> ...
- [HAOI2017模拟]百步穿杨
今天的考试题. 考试的时候因为以前做过还写过题解,然后就以为模型已经很清楚了,然后就开始直接推.最后因为蜜汁自信一定能推出来,然后模型搞错了,只能交个暴力上去,于是这场考试GG. 第一次碰上这道题是在 ...
- 都能看懂的嵌入式linux/android alsa_aplay alsa_amixer命令行用法
前几天在嵌入式linux上用到alsa command,网上查的资料多不给力,只有动手一点点查,终于可以用了,将这个使用方法告诉大家,以免大家少走弯路. 0.先查看系统支持哪几个alsa cmd: l ...
- STS,MyEclipse中Maven配置
本文以STS的环境做讲解,MyEclipse环境和STS差别不大,配置过程相似. STS是解压版的,启动后,可以看到已经有了Maven插件,, 但是,STS也同时给你了一个Maven,但是通常不建议使 ...
- Spring多开发环境配置
在项目开发中,多环境的的配置可以方便在不同环境的切换,减少错误的产生 一.properties文件的多环境配置 properties文件可以根据不同的后缀来区分不同的环境 application.pr ...
- maven打包pom.xml备忘
打包生成可执行jar 包 POM.XML <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=" ...
- Hue的安装与部署
Hue的安装与部署 hadoop hue Hue 简介 Hue是一个开源的Apache Hadoop UI系统,最早是由Cloudera Desktop演化而来,由Cloudera贡献给开源社区,它是 ...
- 【ML数学知识】极大似然估计
它是建立在极大似然原理的基础上的一个统计方法,极大似然原理的直观想法是,一个随机试验如有若干个可能的结果A,B,C,... ,若在一次试验中,结果A出现了,那么可以认为实验条件对A的出现有利,也即出现 ...
- angular js实现开关效果
功能:实现点击排序,再点击排倒序. 实现方法如下 方法一:定义变量实现点击切换true或false,代码为: $scope.lidata = [ {"name ...