第九章 CUDA原子(atomic)实战篇

本文详细介绍了CUDA中的原子操作,包括概念、错误与正确操作示例、原子函数及其位运算,以及在索引处理、数据筛选和计数中的应用。通过学习,读者将能够理解和运用CUDA原子操作进行高效并行计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

学习我的教程专栏,你将绝对能实现CUDA工程化,实现环境安装、index计算、kernel核函数编程、内存优化与steam性能优化、原子操作、nms的cuda算子、yolov5的cuda部署等内容,并开源教程源码。

在本章节中,我们将介绍特殊操作-原子操作(atomic),分为计算原子操作与位相关原子操作。该操作弥补cuda核函数并行对同一内存修改而导致错误结果。另外,介于后面yolo的cuda处理需要使用原子相关操作,特别是按条件筛选保留或处理对应位置的数据。为此,我将在此章节中,介绍有关原子操作内容,并附其源码。

专栏概括

1、cuda教程目录

第一章 指针篇–>点击这里
第二章 CUDA原理篇–>点击这里
第三章 CUDA编译器环境配置篇–>点击这里
第四章 kernel函数基础篇–>点击这里
第五章 kernel索引(index)篇–>点击这里
第六章 kenel矩阵计算实战篇–>点击这里-上篇点击这里-下篇

第七章 kenel实战强化篇–>点击这里
第八章 CUDA内存应用与性能优化篇–>点击这里-上篇点击这里-中篇点击这里-下篇
第九章 CUDA原子(atomic)实战篇–>点击这里
第十章 CUDA流(stream)实战篇–>点击这里
第十一章 CUDA的NMS算子实战篇–>点击这里-上篇点击这里-下篇
第十二章 YOLO的部署实战篇–>点击这里-上篇点击这里-下篇

第十三章 基于CUDA的YOLO部署实战篇–>点击这里

2、cuda教程背景

随着人工智能的发展与人才的内卷,很多企业已将深度学习算法的C++部署能力作为基本技能之一。面对诸多arm相关且资源有限的设备,往往想更好的提速,满足更高时效性,必将更多类似矩阵相关运算交给CUDA处理。同时,面对市场诸多教程与诸多博客岑子不起的教程或高昂教程费用,使读者(特别是小白)容易迷糊,无法快速入手CUDA编程,实现工程化。
因此,我将结合我的工程实战经验,我将在本专栏实现CUDA系列教程,帮助读者(或小白)实现CUDA工程化,掌握CUDA编程能力。学习我的教程专栏,你将绝对能实现CUDA工程化,完全从环境安装到CUDA核函数编程,从核函数到使用相关内存优化与流stream优化,从内存、stream优化到深度学习算子开发(如:nms),从算子优化到模型(以yolo系列为基准)部署。最重要的是,我的教程将简单明了直切主题,CUDA理论与实战实例应用,并附相关代码,可直接上手实战。我的想法是掌握必要CUDA相关理论,去除非必须繁杂理论,实现CUDA算法应用开发,待进一步提高,将进一步理解更高深理论。

3、cuda教程内容

第一章到第三章探索指针在cuda函数中的作用与cuda相关原理及环境配置;

第四章初步探索cuda相关函数编写(globaldevice、__host__等),实现简单入门;

第五章探索不同grid与block配置,如何计算kernel函数的index,以便后续通过index实现各种运算;

第六、七章由浅入深探索核函数矩阵计算,深入探索grid、block与thread索引对kernel函数编写作用与影响,并实战多个应用列子(如:kernel函数实现图像颜色空间转换);

第八章探索cuda内存纹理内存、常量内存、全局内存等分配机制与内存实战应用(附代码),通过不同内存的使用来优化cuda计算性能;

第九章探索cuda原子(atomic)相关操作,并实战应用(如:获得某些自加索引等);

第十章探索cuda流stream相关应用,并给出相关实战列子(如:多流操作等);

第十一到十三章探索基于tensorrt部署yolo算法,我们首先将给出通用tensorrt的yolo算法部署,该部署的前后处理基于C++语言的host端实现,然后给出基于cuda的前后处理的算子核函数编写,最后数据无需在gpu与host间复制操作,实现gpu处理,提升算法性能。

目前,以上为我们的cuda教学全部内容,若后续读者有想了解知识,可留言,我们将根据实际情况,更新相关教学内容。

大神忽略

源码链接地址点击这里
yolov5部署代码链接点击这里
yolov5的cuda部署代码链接:文中链接源码

基于我的代码实测,cuda部署yolov5加速10倍,只想说cuda太香了!!!

一、原子操作概念

1、定义

官网定义:原子函数对驻留在全局或共享内存中的一个 32 位或 64 位字执行读-改-写原子操作。

2、概念解读

cuda原子操作是指在cuda并行计算中,对共享内存或全局内存的原子读、改、写的操作,我们使用原子操作之后,会锁定空间,防止其他的进程访问。在多个线程同时访问同一个内存地址时,原子操作可以保证数据的一致性和正确性,避免出现数据竞争和冲突的情况。由于CUDA的超多线程并行运算的特性,我们可以利用CUDA中的原子操作来优化我们的程序。

总结如下:
①、当线程束的多个线程向同一个内存地址写数据时,程序的正确性是无法保证的,此时需要使用 CUDA 提供的 atomic 函数;
②、原子操作可以保证程序的正确性,但是会造成线程束中线程的串行化 serialization ,执行时间比并行执行要长。这是因为在一个线程完成该操作之前,其他线程将不能对相同的内存地址进行读写操作
③、通过使用共享内存,可以减少对原子操作的使用,从而提高程序的性能;

二、举例说明概念

假设我们想要用GPU统计“int data_0[32] = {1,0, … ,1}”这个数组中 0 和 1 的个数并写入int counter[2]中。

1、错误操作(无原子操作)

如果我们不使用原子操作,错误核函数代码:

extern "C" __global__ void kernel_func(int* counter, int* data_0)
{
    // 计算线程号
    unsigned int block_index = blockIdx.x + blockIdx.y * gridDim.x + blockIdx.z * gridDim.x * gridDim.y;
    unsigned int thread_index = block_index * blockDim.x * blockDim.y * blockDim.z + \
        threadIdx.x + threadIdx.y * blockDim.x + threadIdx.z * blockDim.x * blockDim.y;

    // 统计结果
    int value = data_0[thread_index];
    counter[value] ++;
}

错误结果输出如下:
在这里插入图片描述
可看到,我们想输出为0与1的数量,结果应该均为4,可结果为1。为此,以上代码操作是不允许的。错误实际上是出在了并行上—线程还没有将自己计算的“counter”写回显存变量“counter[value]”,其他线程就已经读取了显存变量“counter[value]”的值,直白说有32条线程(Thread),其数量小于空闲的流处理器(SP)数量时是这样的结果,每个线程都由一个流处理器(SP)来处理。在线程较多时可能多个线程都由一个流处理器(SP)处理。

2、正确操作(原子操作)

如果我们使用原子操作,正确核函数代码:

extern "C" __global__ void kernel_func_correct(int* counter, int* data_0)
{
    // 计算线程号
    unsigned int block_index = blockIdx.x + blockIdx.y * gridDim.x + blockIdx.z * gridDim.x * gridDim.y;
    unsigned int thread_index = block_index * blockDim.x * blockDim.y * blockDim.z + \
        threadIdx.x + threadIdx.y * blockDim.x + threadIdx.z * blockDim.x * blockDim.y;

    // 统计结果
    int value = data_0[thread_index];
    atomicAdd(&counter[value], 1);
}

正确结果输出如下:

在这里插入图片描述
当你的一个线程使用原子加操作在这里, 另一个线程也像做原子加操作的时候, 它就会产生等待. 直到上一个操作完成. 这里会产生一个队列, one by one的执行. 如下图所示.

3、时间测试

为探索使用原子和不使用原子时间gap,我们继续测试,为读者提供参考,其代码如下:

auto T0 = std::chrono::system_clock::now();  //时间函数
    int num = 10000;
    int num_k = 60;
    for (int k = 0; k < num_k; k++) {
        for (int j = 0; j < num; j++) {
            //cudaMemcpy(count, host_count, 2 * sizeof(int), cudaMemcpyHostToDevice);
            kernel_func_error << <N, 1 >> > (count, gpu_buffer);
            //kernel_func_correct << <N, 1 >> > (count, gpu_buffer);
        }
    }    
    auto T1 = std::chrono::system_clock::now();
    float time_kernel = std::chrono::duration_cast<std::chrono::milliseconds>(T1 - T0).count();

    std::cout << "\n\n推理时间:\t " <<  time_kernel/ num_k << "ms\n\n" << endl;

我们循环num_k=60次重复num=10000次,求num_k平均时间,结果未不使用原子操作33.71667ms,使用原子操作37.7667ms,与理论猜测一致,使用原子会比不使用原子快。这是因为使用原子操作后程序具有更大的执行代价,可以通过使用共享内存来加速这些原子累加操作。

完整代码可查看github链接

三、原子函数

CUDA提供了多种原子操作函数,如原子加、原子减、原子与、原子或等,可以在CUDA C/C++中使用。在 CUDA 中,原子操作是一种用于确保多个线程同时访问同一内存地址时的同步机制。原子操作可以确保只有一个线程可以访问内存地址,并且可以避免数据竞争和不确定的结果。我们将介绍2类原子函数,一种常用的原子计算函数;另一种位计算原子函数。

1、计算原子函数

以下是一些常见的原子操作:

①、加法:T atomicAdd(T *address, T val),将一个值加到一个变量上,并返回修改后的值。功能:new = old + val。
②、减法:T atomicSub(T *address, T val),将一个值从一个变量上减去,并返回修改后的值。功能:new = old - val。
③、交换:T atomicExch(T *address, T val),将一个值替换为变量值,并返回替换后的值。功能:new = val。
④、最小值:T atomicMin(T *address, T val),将一个值与一个变量进行比较,并将较小的值存储到变量中,并返回修改后的值。功能:new = (old < val) ? old : val。
⑤、最大值:T atomicMax(T *address, T val),将一个值与一个变量进行比较,并将较大的值存储到变量中,并返回修改后的值。功能:new = (old > val) ? old : val。
⑥、自增:T atomicInc(T *address, T val),将一个变量加1,并返回修改后的值。如果结果超过一个特定的最大值,则将变量重置为0。功能:new = (old >= val) ? 0 : (old + 1)。
⑦、自减:T atomicDec(T *address, T val),将一个变量减1,并返回修改后的值。如果结果小于一个特定的最小值,则将变量重置为0。 功能:new = ((old == 0) || (old > val)) ? val : (old - 1)。
比较-交换(Compare And Swap):T atomicCAS(T *address, T compare, T val); 功能:new = (old == compare) ? val : old。

说明:old代码address地址的值

2、位运算

按位与运算按位与运算符“&”是双目运算符。其功能是参与运算的两数各对应的二进位相与。只要对应的二个二进位都为1时,结果位就为1。该方法适用于二进制操作,若为十进制操作,我的理解为:十进制将转为二进制,然后使用与、或等操作。

按位与运算

参加运算的两个数据,取补码按二进制位进行“与”运算。

运算规则:0&0=0;   0&1=0;    1&0=0;     1&1=1;

即:两位同时为“1”,结果才为“1”,否则为0。

举例说明

对应位置计算,按位进行与运算,如下:

00000101 & 11111011 = 000000001

按位或运算

参加运算的两个数据,按二进制位进行“或”运算。

运算规则:0|0=0;   0|1=1;    1|0=1;     1|1=1;

即:两位同时为“0”,结果才为“0”,否则为1。

举例说明

对应位置计算,按位进行或运算,如下:

00000101 | 11111011 = 11111111

按位异或运算

参加运算的两个数据,按二进制位进行“异或”运算。

运算规则:0^0=00^1=11^0=11^1=0

即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。

举例说明

对应位置计算,按二进制进行异或运算,如下:

00001001^00000101=00001100  

3、位运算代码说明

代码

我们将对十进制数据进行异、或代码解释。

  int a = 6;
    int b = 4;
    std::cout << "\n打印变量a的二进制\n" << a << ":\t";
    printbinary(a);
    std::cout << "\n打印变量b的二进制\n" <<b << ":\t";
    printbinary(b);

    std::cout << "\n打印与、或、异或结果:" << endl;

    printf("\n与结果:\t%d",a&b);
    printf("\n或结果:\t%d", a | b);

    std::cout << "\n打印与的二进制\n";
    printbinary(a & b);
    std::cout << "\n打印或的二进制\n" ;
    printbinary(a|b);

预算结果

在这里插入图片描述
从运算结果可看出,6与4的二进制值,与、或运算是将其转为二进制,然后再二进制对应位置,进行与、或运算规则得到新的二进制,并将其新二进制转为十进制,则获得结果分别为4、6。
至于异或运算,与上类似,如下:

3^5=6,即:0000 0011^0000 0101 = 0000 0110 (二进制转十进制为6) 
9^5=12,:00001001^00000101=00001100 (二进制转十进制为12)  

3、位运算原子函数

以上已解释很多位运算相关规则,其原子的位运算与其类似。我将不在过多解释。

按位与:T atomicAnd(T *address, T val); 功能:new = old & val。
按位或:T atomicOr(T *address, T val); 功能:new = old | val。
按位异或:T atomicXor(T *address, T val); 功能:new = old ^ val。

四、原子实战应用

在第二节的举例说明概念已初步展示原子操作的用法。接下来,我将介绍一些原子操作一些比较有用的实列,这些实列比较有用在于可支撑后面章节使用cuda编写yolo相关逻辑处理方法。

1、原子操作表示索引

使用原子操作,获得输入数据data的值作为索引,并将其对应的值做一定计算。
代码如下:

__global__ void kernel(int* data, int* gpu_output) {
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    // 对共享内存中的数据执行原子加操作
    int index = atomicAdd(&data[tid], 1);
    //printf("%d\n", index);

    if (index > 50) {
        return;
    }
    gpu_output[index] = data[index] * 100;


}

输出结果如下:
在这里插入图片描述
从结果可知,若tid=0,data值从6开始,使用int index = atomicAdd(&data[tid], 1)原子操作后,index索引为6,但data[tid]变成了7,则tid位置6变成7,最终gpu_output[index] = data[index] * 100变为gpu_output[6]=data[7]*100。

2、原子操作偶数位处理与保存

在gpu中若有一串数据,我们需要针对某某些位置的值操作,并将对应位置保留某个指针变量中,此时,我们已不能使用cuda的每个线程操作。为此,我们需要借助原子函数操作,使其满足我们需要功能。
在此,我们使用原子操作,处理输入gpu_buffer[0]数据,保留gpu_buffer[0]的偶数位置的值到gpu_buffer[1]变量中,此时,gpu_buffer[1]保留值只有gpu_buffer[0]的1/2,以此列子代表实现条件索引数据处理与保存。

代码如下:

__global__ void kernel2(int* data, int* gpu_output, int N) {
    int count = data[0];
    //printf("count:%d\n", count);
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    if ((tid + 1) % 2 != 0) { return; } //一个偶数条件限制
    int index = atomicAdd(gpu_output, 1); //当作指针,相当于每次会走一次
    //printf("index:%d\n ", (index));
    if (index >= N / 2) return;
    gpu_output[index] = data[tid];
}

输出结果如下:
在这里插入图片描述
从结果可知,输入为50个数,输出变成25个数,且输出为输入数的偶数位的位置数。

3、原子操作计数

原子操作一个简单列子-统计。使用原子操作,将其count对应地址的值不断加1操作,以此统计list的个数。这里也可以使用atomicInc(T *address, T val)函数。
该代码count初始化值为0,返回为list列表的个数,保留在count中。
代码如下:

__global__ void countValues(float* list, int* count, int n)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        atomicAdd(count, 1);

    }
}

五、总结

以上为cuda原子操作的相关知识和简单代码说明,旨在掌握原子操作偶数位处理与保存,能实现索引和条件处理在gpu上的计算与筛选保存想要位置的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tangjunjun-owen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值