CUDA by example Chapter9 原子性

GPU 高性能编程 CUDA :原子性 NVIDIA GPU 的计算功能集:NVIDIA 将 GPU 支持的各种功能统称为计算功能集nvcc -arch=sm_11 //告诉编译器需要1.1版本或者更高 nvcc -arch=sm_12 //告诉编译器需要1.2版本或者更高原子操作简介:在执行过程中不能分解为更小的部分,将满足这种条件限制的操作称为原子操作,当有数千个线程在内存访问上发生竞争时,这些操作可以保证在内存上实现安全的操作... 阅读详情


与在操作系统中的线程竞争相似,GPU编程各个线程之间也存在竞态条件,为了保证程序按照我们设计的逻辑运行,原子性操作必不可少。
本章以计算直方图为例,说明在GPU上使用原子操作的方法。

在CPU上计算直方图

这是平凡的

// 代码9.4.1在CPU上计算直方图
//时间:2019.07.30
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <iostream>
#include "book.h"

#define SIZE (10*1024*1024)

int main()
{
	unsigned char *buffer = (unsigned char *)big_random_block(SIZE);
	unsigned int histo[256];

	//对直方图数组进行初始化
	for(int i = 0; i < 256; i++)
	{
		histo[i] = 0;
	}
	//CPU统计直方图信息
	for (int i = 0; i < SIZE; i++)
	{
		histo[buffer[i]]++;
	}

	//验证直方图统计的正确性
	long histoCount = 0;
	for (int i = 0; i < 256; i++)
	{
		histoCount += histo[i];
	}
	printf("Histogram Sum:%ld\n", histoCount);

	free(buffer);
	system("pause");
	return 0;

}

在这里插入图片描述
显然,10485760 = 1024102410,验证通过!

在GPU上计算直方图

通过GPU的多个线程来处理输入缓冲区的不同部分,在这个过程中,多个线程可能同时对输出直方图的同一个元素进行递增。在这种情况下,我们需要通过原子递增操作来避免线程竞争问题。

一些重要函数及技巧

1. cudaMemset
类似于memset,cudaMemset对GPU上的内存初始化数值,如果初始化失败,将返回一个错误码,用法示例:

cudaMemset(dev_histo, 0, sizeof(int)* 256);//对GPU上的内存初始化数值

2. block数量的选择技巧

  • 由于直方图包含了256个元素,因此threadsPerBlock可以设置成256,这样做方便又高效。
  • block数量的选择
    假设我们有100MB数据,也就是104857600个字节。我们可以用一个block,每个thread处理409600个数据元素;也可以用409600个block,每个thread处理一个数据元素。
    通过一些性能实验,发现当block的数量为GPU中处理器数量的2倍时,将达到最优性能。

3. GPU原子递增操作
atomicAdd(addr,y)是CUDA提供的原子递增操作函数,它包含两个参数,第一个参数是要递增的变量地址,第二个参数是每次递增的步长。
这个函数将生成一个原子操作序列,这个操作序列包括读取地址addr处的值,将这个值增加y,以及将结果保存到addr。
底层硬件将确保执行这些操作时,其他任何线程都不会读取或者写入地址addr上的值,示例如下:

atomicAdd(&histo[buffer[i]], 1);//每次递增1

使用GPU全局内存原子操作计算直方图

// 代码9.4.2.1在GPU上计算直方图(全局内存)
//时间:2019.07.30
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <iostream>
#include "book.h"

#define SIZE (10*1024*1024)


__global__ void histo_kernal(unsigned char *buffer, long size, unsigned int *histo)
{
	int i = threadIdx.x + blockIdx.x*blockDim.x;
	int stride = gridDim.x*blockDim.x;
	while (i < size)
	{
		atomicAdd(&histo[buffer[i]], 1);//原子性操作
		i += stride;
	}

}


int main()
{
	unsigned char *buffer = (unsigned char *)big_random_block(SIZE);

	//记录起始事件
	cudaEvent_t start, stop;
	cudaEventCreate(&start);
	cudaEventCreate(&stop);
	cudaEventRecord(start, 0);

	//在GPU上为文件数据分配内存
	unsigned char *dev_buffer;
	unsigned int *dev_histo;

	cudaMalloc((void **)&dev_buffer, SIZE);
	cudaMemcpy(dev_buffer, buffer, SIZE, cudaMemcpyHostToDevice);

	cudaMalloc((void **)&dev_histo, sizeof(int)* 256);
	cudaMemset(dev_histo, 0, sizeof(int)* 256);//对GPU上的内存初始化数值

	//执行kernal函数,经验发现当线程块的数量为GPU中处理器数量的2倍时,将达到最优性能
	cudaDeviceProp prop;
	cudaGetDeviceProperties(&prop, 0);
	int blocks = prop.multiProcessorCount;
	histo_kernal << <blocks * 2, 256 >> >(dev_buffer, SIZE, dev_histo);



	//将GPU统计得到的直方图copy到CPU
	unsigned int histo[256];
	cudaMemcpy(histo, dev_histo, sizeof(int)* 256,cudaMemcpyDeviceToHost);

	//得到停止时间并显示计时结果
	cudaEventRecord(stop, 0);
	cudaEventSynchronize(stop);
	float elapsedTime;
	cudaEventElapsedTime(&elapsedTime, start, stop);
	printf("Time to generate:%3.1f ms\n", elapsedTime);

	//验证直方图总和是否正确
	long histoCount = 0;
	for (int i = 0; i < 256; i++)
	{
		histoCount += histo[i];
	}
	printf("Histogram Sum:%ld\n", histoCount);

	//验证与CPU得到的是相同的计数值
	for (int i = 0; i < SIZE; i++)
	{
		histo[buffer[i]]--;
	}
	for (int i = 0; i < 256; i++)
	{
		if (histo[i] != 0)
			printf("Failure ar %d!\n", i);
	}

	//释放CUDA事件,主机内存,GPU内存
	cudaEventDestroy(start);
	cudaEventDestroy(stop);
	cudaFree(dev_histo);
	cudaFree(dev_buffer);
	free(buffer);

	system("pause");
	return 0;
}

在这里插入图片描述

使用GPU共享内存原子操作计算直方图

全局内存dev_histo只有256个字节,当数千个线程尝试访问如此少量的内存位置时,将发生大量的竞争。为了保证递增操作的原子性,对相同内存位置的操作都将被硬件串行化,这是效率低下的。
使用共享内存可以有效解决以上问题,思路如下:
将直方图计算分为两个阶段,在第一个阶段,使用共享内存,每个并行block将独立计算它所处理数据的直方图,每个block中的所有线程在该block中对共享内存缓冲区的操作仍然需要保证原子性。显然,在一个block中只有256个线程在256个地址上发生竞争,这将极大地减少在使用全局内存时在数千个线程之间发生竞争的情况。在第二个阶段,将第一个阶段每个block产生的统计结果,加和到最终直方图相应的元素上,同时,必须要注意的是,第一个阶段完成之后需要线程同步,等到所有block都完成了局部计算之后,第二个阶段才可以进行。

// 代码9.4.2.2在GPU上计算直方图(共享内存)
//时间:2019.07.30
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <iostream>
#include "book.h"

#define SIZE (10*1024*1024)


__global__ void histo_kernal(unsigned char *buffer, long size, unsigned int *histo)
{
	//每个block中都包含一个独立的共享内存temp,每个block中有256个thread
	__shared__ unsigned int temp[256];
	temp[threadIdx.x] = 0;//初始化为0
	__syncthreads();

	//每个block中计算一下计数值
	int i = threadIdx.x + blockIdx.x*blockDim.x;
	int stride = gridDim.x*blockDim.x;
	while (i < size)
	{
		atomicAdd(&temp[buffer[i]], 1);//原子性操作
		i += stride;
	}
	__syncthreads();

	//将每个block中的temp内容汇总到histo
	atomicAdd(&(histo[threadIdx.x]), temp[threadIdx.x]);

}


int main()
{
	unsigned char *buffer = (unsigned char *)big_random_block(SIZE);

	//记录起始事件
	cudaEvent_t start, stop;
	cudaEventCreate(&start);
	cudaEventCreate(&stop);
	cudaEventRecord(start, 0);

	//在GPU上为文件数据分配内存
	unsigned char *dev_buffer;
	unsigned int *dev_histo;

	cudaMalloc((void **)&dev_buffer, SIZE);
	cudaMemcpy(dev_buffer, buffer, SIZE, cudaMemcpyHostToDevice);

	cudaMalloc((void **)&dev_histo, sizeof(int)* 256);
	cudaMemset(dev_histo, 0, sizeof(int)* 256);//对GPU上的内存初始化数值

	//执行kernal函数,经验发现当线程块的数量为GPU中处理器数量的2倍时,将达到最优性能
	cudaDeviceProp prop;
	cudaGetDeviceProperties(&prop, 0);
	int blocks = prop.multiProcessorCount;
	histo_kernal << <blocks * 2, 256 >> >(dev_buffer, SIZE, dev_histo);



	//将GPU统计得到的直方图copy到CPU
	unsigned int histo[256];
	cudaMemcpy(histo, dev_histo, sizeof(int)* 256, cudaMemcpyDeviceToHost);

	//得到停止时间并显示计时结果
	cudaEventRecord(stop, 0);
	cudaEventSynchronize(stop);
	float elapsedTime;
	cudaEventElapsedTime(&elapsedTime, start, stop);
	printf("Time to generate:%3.1f ms\n", elapsedTime);

	//验证直方图总和是否正确
	long histoCount = 0;
	for (int i = 0; i < 256; i++)
	{
		histoCount += histo[i];
	}
	printf("Histogram Sum:%ld\n", histoCount);

	//验证与CPU得到的是相同的计数值
	for (int i = 0; i < SIZE; i++)
	{
		histo[buffer[i]]--;
	}
	for (int i = 0; i < 256; i++)
	{
		if (histo[i] != 0)
			printf("Failure ar %d!\n", i);
	}

	//释放CUDA事件,主机内存,GPU内存
	cudaEventDestroy(start);
	cudaEventDestroy(stop);
	cudaFree(dev_histo);
	cudaFree(dev_buffer);
	free(buffer);

	system("pause");
	return 0;
}

在这里插入图片描述
显然,性能是确有提升的。

Ubuntu离线情况下安装gcc编译器 Ubuntu离线情况下安装gcc编译器1.系统版本2.安装说明3.异常处理 1.系统版本 (1)Ubuntu系统版本:20.04.1LTS; (2)gcc编译器版本:gcc-9.3.0(下载链接为https://download.youkuaiyun.com/download/qq_18350351/13762214); 当前可找到的gcc最新版本为gcc-10,但是笔者在安装gcc-10版本时发现一些依赖项无法安装,因此选取了gcc-9版本。如果是在联网情况下安装,直接使用apt相关的安装指令即可。 2.安装说明 安 阅读详情

相关推荐

关于Win11安装Ubuntu子系统报错:WslRegisterDistribution failed with error 解决办法

Win11安装Ubuntu子系统报错:WslRegisterDistribution failed with error

qq_51908382的博客 3万+

CUDA原子操作

这是因为更广泛的分布范围上的原子操作有利于利用多个能执行原子操作的单元,以及每个原子操作单元上面的竞争性的原子事务也相应减少了。在这个代码中,1 000个线程对同一内存区域进行原子+1操作,这1 000次相同区域上的操作,每次都将从逻辑上安全地完整执行。该函数会从逻辑上保证,每个调用它的线程对相同的内存区域上的“读取旧值-累加-回写新值”操作是不可被其他线程扰乱的原子性的整体完成的。在main函数中,具有10个元素的数组被初始化成0值,然后传递给了内核,但现在,内核中的代码将执行原子累加操作。

taifyang的博客 504

告别配置烦恼!保姆级教程:在Windows 10/11上为QT5.14.2配置MSVC2017编译器(附VS2022组件避坑指南)

本文提供了一份详细的保姆级教程,指导如何在Windows 10/11上为QT5.14.2配置MSVC2017编译器,包括VS2022组件避坑指南。从环境准备到关键配置,再到调试工具验证和Qt Creator中的编译器设置,一步步解决常见问题,帮助开发者快速搭建稳定可靠的开发环境。

weixin_30615767的博客 503

CUDA编程模型系列八(原子操作 / 规约 / 向量元素求和)

本期我介绍了cuda 当中规约算法的一种情况, 也是小何尚职业生涯中的第一道面试题, 计算数组中所有元素的和.本系列视频目的是帮助开发者们一步步地学会利用CUDA编程模型加速GPU应用, 我们的口号是: 让GPU飞起来。CUDA编程模型系列八(原子操作 / 规约 / 向量元素求和)

专注于人工智能领域的小何尚 1686

二维数字图像相关算法软件Ncorr的使用心得

二维开源软件Ncorr的DIC分析操作过程详解。

m0_72612723的博客 3万+

IsaacLab从入门到精通(二) 导入自定义机器人

IsaacLab系列第二讲,导入自定义的机器人及设置

DejaWu33的博客 8469

CUDA By Example(七)——原子性

简单介绍CUDA的原子操作,并利用直方图算法,详细解释如何在GPU上使用原子操作。

hhhcbw的博客,欢迎各位来访 977

基于Multisim的智能交通灯控制系统仿真与实现(含动态闪烁功能)

本文详细介绍了基于Multisim的智能交通灯控制系统仿真与实现,重点解析了动态闪烁功能的电路设计。通过555定时器、74LS192计数器和JK触发器等核心模块,实现了主干道与支干道的交替通行及黄灯1Hz闪烁功能,为交通灯控制系统设计提供了完整的仿真方案。

weixin_29174013的博客 426

cuda学习笔记(5)

目录原子操作(Atomics) 笔记(2)中提到当cuda需要对同一个内存地址中的值进行读写访问时,需要原子操作(Atomics)(类似cpu多线程的互斥锁mutex或原子变量),本章通过一个列子介绍原子操作。 原子操作(Atomics) 有些操作不能被拆分, 多线程要针对同一内存地址进行操作,这时需要对该内存使用原子操作,来保证线程对资源有暂时的”独占性”, 避免计算错误。原子操作是把双刃剑,因...

Frizy的博客 1154

S7-1500——(一)西门子PLC编程从入门到精通4、SCL间接寻址

学习目标是在掌握博途平台SCL编程语言的基本使用基础之上,进一步了解SCL语言环境下的间接寻址过程,初步具备编写复杂控制程序的能力;在编写plc程序过程中, 需要对plc中的数据进行访问,这就是寻址;直接寻址间接寻址;直接寻址直接寻址就是直接对数据的存储单元进行访问;根据表达方式的不同,可分为绝对寻址和符号寻址;对于plc中的标准数据存储区包括IQMTCL以及标准数据块,既可以使用绝对寻址,也可以使用符号寻址;绝对寻址在使用绝对寻址时,可以寻址的宽度包括位,字,节字,双字;

大小曲奇的博客 2121

加密算法:深度解析Ed25519原理

在 Solana 开发过程中,我一直对 Ed25519 加密算法 如何生成公钥、签名以及验证签名的机制感到困惑。为了弄清这一点,我查阅了大量相关资料,终于对其流程有了更清晰的理解。在此记录实现过程,方便日后查阅。

盹猫的博客 1万+

CUDA by example Chapter10 流

流为什么要使用CUDA流页锁存主机内存CUDA流使用单个CUDA流使用多个CUDA流GPU工作调度机制高效地使用多个CUDA流 为什么要使用CUDA流 前面章节实现的GPU并行性都是把同一个任务分成可以并行执行的子任务,利用GPU中数量众多的cuda核来实现单个任务的并行性。而如果要实现任务并行性,就需要使用CUDA流来实现。 页锁存主机内存 1. 页锁存内存的概念及用途 页所存内存也称为固定内...

wolfcsharp的博客 336

Android oom_adj 更新原理(一)

通过之前的两篇博文《lmkd 机制详解》和《lmkd中adj score的算法剖析》中,根据计算出来的 min_score_adj 选择大于该值的 oom_adj 的进程进行 kill 处理。oom_adj 随着应用状态的不同其值也是伴随着变化的。本文将通过源码剖析 oom_adj 更新的原理。

私房菜 2656

《CUDA By Example》【Chapter 09】原子性?

9.1 概述 了解不同NVIDIA GPU的计算功能集 了解原子操作以及为什么需要使用 了解如何在CUDA C核函数中执行带有原子操作的运算 9.2 计算功能集 不同架构的CPU有不同的功能和指令集(如MMX,SSE,SSE2),对于CUDA支持的GPU也一样。NVIDIA将GPU支持的各种功能统称为计算功能集(Compute Capability)。 9.2.1 NVIDIA...

wondervictor的博客 365

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

CUDA by example Chapter3 CUDA C简介

CUDA C简介CPU上的Hello WorldGPU上的Hello World__global__修饰符:向kernal()中传递参数cudaMalloc()函数:cudaFree()函数:cudaMemcpy()函数:查询设备信息cudaGetDeviceCount()函数:结构体cudaDevicePropcudaGetDeviceProperties()函数:设备属性的使用:选择一个符合条...

wolfcsharp的博客 394

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

《CUDA By Example》【Chapter 10】流?

10.1 概述 并行性分数据并行性和任务并行性。GPU是SIMD天生的数据并行,同时也能支持类似于CPU多线程应用程序中的任务并行性。 GPU任务并行性不像CPU上那样灵活,但仍然可以进一步提高GPU上的运行速度。本章,介绍CUDA流,以及如何通过流在GPU上同时执行多个任务。 了解如何分配也锁定(Page-Locked)类型的主机内存 了解CUDA流的概念 了解如何使用CUDA流...

wondervictor的博客 324

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

CUDA by example Chapter5 线程协作

线程协作并行线程块的分解使用线程实现GPU上的矢量求和 并行线程块的分解 使用线程实现GPU上的矢量求和

wolfcsharp的博客 388

CUDA by example Chapter4 CUDA C并行编程

CUDA C 并行编程CPU矢量求和运算GPU矢量求和运算尖括号语法<<CPU矢量求和运算 /* //代码4.2.1.1基于CPU的矢量求和运算 //时间:2019.07.20 */ #include "cuda_runtime.h" #include "device_launch_parameters.h" #include <iostream> #define N 1...

wolfcsharp的博客 369

《CUDA By Example》【Chapter 05】线程协作 ?

概述 本章介绍代码在各个并行副本之间的通信和协作。 1,了解不同线程之间的通信机制; 2,了解并行执行线程的同步机制; 5.2 并行线程块的分解 add&lt;&lt;&lt;N,1&gt;&gt;&gt;( dev_a, dev_b, dev_c); //第一个参数为想要启动的线程块数量; //第二个参数为CUDA运行时在每个线程块中创建的线程数量; //共创建了N个线程块,N*1...

wondervictor的博客 294

《CUDA By Example》【Chapter 04】CUDA C 并行编程

GPU计算的应用前景在很大程度上取决于能否从很多问题中发掘出大规模并行性。 add_loop_cpu.cu #include &amp;amp;amp;amp;quot;../common/book.h&amp;amp;amp;amp;quot; #define N 10 void add( int *a, int *b, int *c ) { int tid = 0; // this is CPU zero, so we start at ze...

wondervictor的博客 299

《CUDA By Example》【Chapter 06】常量内存与事件 ?

6.1 本章目标 高级些的功能,通过GPU上特殊的内存区域来加速应用程序的执行。其中一种内存区域:常量内存(Constant Memory)。介绍一种增强CUDA C应用程序性能的方法,了解如何通过事件来测量应用程序的性能。 如何在CUDA C中使用常量内存 了解常量内存的性能性能 学习使用CUDA事件来测量应用程序的性能 6.2 常量内存 由于GPU有很多ALU, 所以计算能力往...

wondervictor的博客 330

CUDA by example Chapter7 纹理内存

纹理内存纹理内存简介热传导模拟简单的传热模型代码中温度更新的计算GPU不使用纹理内存的热传导模拟计算GPU使用一维纹理内存的热传导模拟计算GPU使用二维纹理内存的热传导模拟计算 纹理内存简介 和常量内存一样,纹理内存是另一种类型的只读内存。纹理内存是专门为那些在内存访问模式中存在大量空间局部性的图形应用程序而设计的。假如在某个计算应用程序中,一个线程读取的位置与邻近线程读取的位置"非常接近",使用...

wolfcsharp的博客 599

《CUDA By Example》【Chapter 07】纹理内存 ?

7.1 本章目标 纹理内存和常量内存一样,都是只读内存,用来减少带宽流量,提升性能。 1,了解纹理内存的特性 2,了解如何在CUDA C中使用一维纹理内存 3,了解如何在CUDA C中使用二维纹理内存 7.2 纹理内存简介(Texture Memory) 7.3 热传导模拟...

wondervictor的博客 357

CUDA by example Chapter11 多GPU系统上的CUDA C

多GPU系统上的CUDA C零拷贝主机内存通过零拷贝内存实现点积运算零拷贝内存的性能使用多个GPU 零拷贝主机内存 零拷贝主机内存是另一种固定内存,它也不能被CPU从物理内存交换出去或者重新定位。同时,这种内存除了可以用于主机与GPU之间的内存复制外,还可以在CUDA C核函数中直接访问这种类型的主机内存。由于这种内存不需要复制到GPU,因此也称为零拷贝内存。 通过零拷贝内存实现点积运算 1. 测...

wolfcsharp的博客 400

《CUDA By Example》【Chapter 11】多GPU系统上的CUDA C?

11.1 概述 了解如何分配和使用零拷贝内存(zero-copy memory) 了解如何在同一个应用程序中使用多个GPU 了解如何分配和使用可移动的固定内存(Portable pinned Memory) 11.2 零拷贝主机内存 固定内存(页锁定内存)能确保不会交换出物理内存。我们通过调用cudaHostAlloc()来分配这种内存,并且传递参数cudaHostAllocDefa...

wondervictor的博客 613

《CUDA By Example》【Chapter 03】CUDA C简介

3.2 第一个CUDA程序 simple_kernel.cu #include &amp;amp;quot;../common/book.h&amp;amp;quot; __global__ void kernel( void ) { } int main( void ) { kernel&amp;amp;amp;lt;&amp;amp;amp;lt;&amp;amp;amp;lt;1,1&amp;amp;amp;gt;&amp;amp

wondervictor的博客 362
上一篇: CUDA by example Chapter7 纹理内存
下一篇: CUDA by example Chapter10 流
Leo-Ma
博客等级 码龄11年 1701粉丝 124原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值