cuda逐步优化实现reduce sum 操作

news2024/9/22 3:35:39

归约是一种常见的数据并行原语,它将数组中的元素通过某种二元操作(如加法)合并成一个单一的值。通过逐步展示不同的CUDA实现版本,来演示重要的优化策略。
由于规约的算术操作很简单,对算力要求不高,因此我们逐步优化目标是尽可能达到最高的带宽利用率,基本想法是:

  • 树状归约方法:在每个线程块内使用基于树的方法进行局部归约,然后需要处理如何跨线程块通信部分结果。

  • 全局同步问题:CUDA没有全局同步机制,因为这样做在硬件上成本高昂,并且会限制程序运行的线程块数量,影响整体效率。

  • 内核分解:通过分解计算为多个内核调用来避免全局同步,内核启动点作为全局同步点,具有较低的硬件和软件开销。

  • 优化目标:对于归约操作,由于其算术强度很低(每个加载的元素仅有一次浮点操作),优化目标是达到峰值带宽。

基础实现

__global__ void reduceSum(int *g_idata, int* g_odata)
{
    extern __shared__ int sdata[];
    uint tid = threadIdx.x;
    uint i = blockIdx.x*blockDim.x+threadIdx.x;

    sdata[tid] = g_idata[i];
    // printf("blockIdx=%d,sdata[%d]=%d ",blockIdx.x,tid,sdata[tid]);

    __syncthreads();

    for(uint s=1; s<blockDim.x; s*=2){
        if (tid %(2*s) == 0){
            sdata[tid] += sdata[tid+s];
        }
        __syncthreads();
    }
    if(tid ==0) {
        g_odata[blockIdx.x] = sdata[0];
        // atomicAdd(g_odata, sdata[0]);
    }
}

在这里插入图片描述

Warp thread divergent

在 CUDA 编程中,高度发散的 warps 和使用 %(取模)运算符都会对性能产生负面影响。

高度发散的 Warps Warp 是 CUDA 中的一个基本执行单元。一个 Warp 包含 32 个线程,这些线程在同一个流多处理器(SM)中并行执行相同的指令。
如果一个 Warp 中的所有线程都执行相同的指令,则 Warp 是一致的,性能最好。
Warp 发散 发生在同一个 Warp 中的线程执行不同的指令路径时。通常是因为条件分支语句(如 if-else)导致不同线程走不同的代码路径。

  • 当 Warp 发散时,CUDA 硬件必须序列化不同的执行路径。这意味着,虽然所有线程在逻辑上是并行的,但实际上它们不得不逐路径地执行不同的指令,这大大降低了并行效率。
  • 举例来说,如果一个 Warp 中一半的线程执行一个路径,另一半执行另一个路径,那么两个路径将被顺序执行,每个路径只利用了一半的线程,效率降低。

% 运算符很慢

  • % 运算符在很多硬件架构上实现起来比较复杂和耗时,因为它通常需要进行除法运算,而除法比加法、减法和乘法慢很多。
  • 在 CUDA 编程中,特别是对于 GPU 的流多处理器(SM)来说,整数除法和取模操作更为耗时,因为这些操作需要更多的时钟周期来完成。

解决方案

  • 减少 Warp 发散
    • 最小化条件分支:尽量减少 if-else 语句的使用,特别是在 Warp 内部。
    • 数据重构:尝试重构数据,使得同一个 Warp 中的线程能够执行相同的指令。
    • 避免复杂的条件判断:如果条件判断无法避免,尝试使用其它算法或数据结构来最小化发散。
  • 优化取模操作
    • 使用位操作:如果取模的数是 2 的幂,可以使用位操作来代替 %。例如,x % 4 可以替换为 x & 3。
    • 查找表:对于小范围的取模操作,可以使用查找表来替代计算。
    • 简化算法:如果可能,重构算法以减少或避免取模操作。
__global__ void reduceSum1(int *g_idata, int* g_odata)
{
    extern __shared__ int sdata[];
    uint tid = threadIdx.x;
    uint i = blockIdx.x*blockDim.x+threadIdx.x;

    sdata[tid] = g_idata[i];
    // printf("blockIdx=%d,sdata[%d]=%d ",blockIdx.x,tid,sdata[tid]);

    __syncthreads();

    for(uint s=1; s<blockDim.x; s*=2){
        int index = 2*s*tid;
        
        if (index< blockDim.x){
            sdata[tid] += sdata[tid+s];
        }
        __syncthreads();
    }
    if(tid ==0) {
        // g_odata[blockIdx.x] = sdata[0];
        atomicAdd(g_odata, sdata[0]);
    }
}

Bank conflict

在这里插入图片描述
在归约过程中,我们确保访问模式没有bank冲突。在 CUDA 设备上,默认情况下每个内存Bank有 32 个 int 元素,因此访问模式 sdata[tid] 和 sdata[tid + s] 在没有填充的情况下通常不会导致bank冲突,特别是当 blockDim.x 是 2 的幂时。

// solve bank conflict, use sequence addressing
__global__ void reduceSum2(int *g_idata, int* g_odata)
{
    extern __shared__ int sdata[];
    uint tid = threadIdx.x;
    uint i = blockIdx.x*blockDim.x+threadIdx.x;

    sdata[tid] = g_idata[i];
    // printf("blockIdx=%d,sdata[%d]=%d ",blockIdx.x,tid,sdata[tid]);

    __syncthreads();

    for(uint s=blockDim.x/2; s>0; s>>=1){
        if (tid< s){
            sdata[tid] += sdata[tid+s];
        }
        __syncthreads();
    }
    if(tid ==0) {
        // g_odata[blockIdx.x] = sdata[0];
        atomicAdd(g_odata, sdata[0]);
    }
}

Unroll last warp

指令开销(Instruction Overhead) 是指那些与核心计算无直接关系的辅助指令的开销。这些辅助指令包括地址计算、循环控制、条件判断等。尽管这些指令在 CPU 编程中可能看起来很轻量,但在 GPU 上,由于大量线程的并行执行,甚至少量的开销也会累积成显著的性能瓶颈。

如何缓解这些开销

  1. 减少地址计算
    尽量减少复杂的索引计算,将索引计算移出循环体或频繁执行的代码块中。
    使用共享内存来存储中间结果,减少全局内存访问的复杂索引计算。
  2. 优化循环
    尽量减少循环的层数和每次迭代的复杂度。
    使用 unrolling 技术手动展开循环,减少循环控制指令的开销。
  3. 减少分支和条件判断
    避免 Warp 分歧,尽量减少条件分支。
    使用更简单的逻辑和数据结构来避免复杂的条件判断。
// unroll last warp, minimize loop and other condition code, and address arithmetric instruction 
__global__ void reduceSum3(int *g_idata, int* g_odata)
{
    extern __shared__ int sdata[];
    uint tid = threadIdx.x;
    uint i = blockIdx.x*blockDim.x+threadIdx.x;

    sdata[tid] = g_idata[i];
    // printf("blockIdx=%d,sdata[%d]=%d ",blockIdx.x,tid,sdata[tid]);

    __syncthreads();

    for(uint s=blockDim.x/2; s>32; s>>=1){
        if (tid< s){
            sdata[tid] += sdata[tid+s];
        }
        __syncthreads();
    }

    //last 32 thread all in the same warp, so do not need to use syncthread in a single warp
    // this saves useless work in all warps, not just the last one
    // without unrolling, all warps execute every iteration of the for loop and if statement
    if (tid<32) {
    	//use volatile to avoid compiler optimization, keep 
    	//fetch value from the memory(not the register) everytime
        volatile int* vdmem = sdata;
        vdmem[tid] += vdmem[tid+32];
        vdmem[tid] += vdmem[tid+16];
        vdmem[tid] += vdmem[tid+8];
        vdmem[tid] += vdmem[tid+4];
        vdmem[tid] += vdmem[tid+2];
        vdmem[tid] += vdmem[tid+1];
    };

    if(tid ==0) {
        // g_odata[blockIdx.x] = sdata[0];
        atomicAdd(g_odata, sdata[0]);
    }
}

Unroll all loop

template<unsigned int blockSize>
__device__ void warp_reduce(volatile int* sdata, int tid)
{
    sdata[tid] += sdata[tid+32];
    sdata[tid] += sdata[tid+16];
    sdata[tid] += sdata[tid+8];
    sdata[tid] += sdata[tid+4];
    sdata[tid] += sdata[tid+2];
    sdata[tid] += sdata[tid+1];
}

//unroll all loop code, use template to keep kernel be generic
template<unsigned int blockSize>
__global__ void reduceSum4(int *g_idata, int* g_odata)
{
    extern __shared__ int sdata[];
    uint tid = threadIdx.x;
    uint i = blockIdx.x*blockDim.x+threadIdx.x;
    uint gridSize = 2*blockSize*gridDim.x;
    sdata[tid] = 0;
    uint n = 4000000;
    while(i<n){
        sdata[tid] +=g_idata[i] + g_idata[i+blockSize];
        i +=gridSize;
    }
    __syncthreads();

    //blocksize condition judgement will be evaluated at compile time
    if (blockSize >= 1024) { if (tid < 512) { sdata[tid] += sdata[tid + 512]; } __syncthreads(); }
    if (blockSize >= 512) { if (tid < 256) { sdata[tid] += sdata[tid + 256]; } __syncthreads(); }
    if (blockSize >= 256) { if (tid < 128) { sdata[tid] += sdata[tid + 128]; } __syncthreads(); }
    if (blockSize >= 128) { if (tid < 64) { sdata[tid] += sdata[tid + 64]; } __syncthreads(); }

    //last 32 thread all in the same warp, so do not need to use syncthread in a single warp
    // this saves useless work in all warps, not just the last one
    // without unrolling, all warps execute every iteration of the for loop and if statement
    if (tid<32) warp_reduce<blockSize>(sdata, tid);

    if(tid ==0) {
        // g_odata[blockIdx.x] = sdata[0];
        atomicAdd(g_odata, sdata[0]);
    }
}

  • 线程步长和循环遍历:
    每个线程在初始化时会根据 blockIdx.x 和 threadIdx.x 计算其全局索引 i。随后,通过 while (i < n) 循环确保每个线程在遍历其负责的所有数据。i += gridSize 会使线程跳到其下一个负责的数据块。

  • 合并内存访问:
    当每个线程遍历数据时,gridSize 作为步长使得每个线程块中的线程能够均匀分布在全局数据范围内,确保内存访问的合并。

  • 减少内存访问次数:每个线程在while循环中尽可能多地从全局内存加载数据到共享内存,减少了对全局内存的访问次数。全局内存访问通常比共享内存访问要慢得多,因此减少全局内存访问可以提高性能(如果输入gridDim是一维的,while 循环只能执行一次,并不能累积数据)。

  • 提高内存访问的效率:通过while循环,每个线程可以加载更多的数据,这样可以通过增加每个线程的工作量来提高内存访问的效率。在CUDA编程中,内存访问的效率(如内存吞吐量)对于性能至关重要。

  • 减少内核启动开销:通过让每个线程做更多的工作,可以减少需要启动的内核数量。内核启动有一定的开销,减少内核数量可以减少这种开销。

  • 算法级联(Algorithm Cascading):这种方法实际上是算法级联的一个应用,即将顺序算法和并行算法结合起来。每个线程加载多个元素,然后在共享内存中进行树状归约。这种方法可以减少递归内核调用的层级,从而减少内核启动的开销。

  • 保持内存访问的连贯性:在while循环中,通过使用gridSize作为步长,可以保持内存访问的连贯性(coalescing),这有助于进一步提高内存访问的性能。

  • 减少线程空闲时间:在前面的kernel中,有一半的线程在第一次循环迭代时是空闲的。而在这个kernel中,通过while循环确保了所有线程都在忙碌地执行工作,从而更充分地利用了GPU的计算资源。

Average elasped time:(0.000252) second, N size:(4000000), bandwidth:(63.516257 GB/s)
Average elasped time:(0.000200) second, N size:(4000000), bandwidth:(80.102532 GB/s)
Average elasped time:(0.000197) second, N size:(4000000), bandwidth:(81.168833 GB/s)
Average elasped time:(0.000161) second, N size:(4000000), bandwidth:(99.423344 GB/s)
Average elasped time:(0.000172) second, N size:(4000000), bandwidth:(92.885010 GB/s)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1982702.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文件上传和下载

要想实现文件上传和下载&#xff0c;其实只需要下述代码即可&#xff1a; 文件上传和下载 import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import com.example.common.Result; import org.springframework.web.bind.annotation.*; import org.sprin…

SQL注入sqli-labs-master关卡二

第二关如下&#xff1a; 查看页面与第一关差不多的样子&#xff0c;再查看PHP源码&#xff0c;与第一关差不多只是其中的查询处有不同。&#xff08;查看源码是为了更好的判断出该页面有什么漏洞&#xff09;其中没有单引号所以不用添加单引号去闭合去逃离单引号&#xff0c;说…

【生成式AI-一-生成式AI到底在说什么】

成式AI到底在说什么 什么是生成式人工智能生成式人工智能、机器学习、深度学习的关系chat-gpt 到底是如何实现对话的&#xff1f; 今天主要来看到底生成式AI是什么&#xff0c;语言模型是如何实现生成这个功能的&#xff1f; 什么是生成式人工智能 现在人工智能能做的事情很多…

pxe环境下的无人值守自动安装

0. 环境部署前的准备 1.rhel7的主机 2.开启主机图形 3.配置网络可用 4.关闭vmware dhcp功能 5.关闭防火墙以及selinux getenforce要为disable状态 grubby --update-kernel ALL --args selinux0 systemctl disable --now firewalld 1.kickstart自动安装脚本制作 我们想要…

SQL注入之webshell上传

首先webshell上传就是利用MySQL的文件读写注入而实现,要想上传webshell就得了解文件读写注入的原理。文件读写注入就是利用文件的读写权限进行注入&#xff0c;它可以写一句话木马&#xff0c;也可以读取文件系统的敏感信息。 文件读写注入的条件&#xff1a; 1.高版本的MYSQ…

Axure导入ElementUI元件库——提升原型设计效率与质量

在快速迭代的互联网产品开发过程中&#xff0c;高质量的原型设计是确保项目顺利进行的关键一步。Axure RP&#xff0c;作为一款强大的原型设计工具&#xff0c;以其丰富的交互功能和易用的界面设计&#xff0c;深受设计师和开发者的喜爱。而ElementUI&#xff0c;作为一套为开发…

Ubuntu配置carla docker环境

前言: 本文只在以下设备成功运行, 其他设备不保证能成功, 可以参考在自己设备进行配置 环境 ubuntu 20.04carla 0.9.15gpu 3060(notebook) 安装显卡驱动&nvidia-container-toolkit 显卡驱动 安装完成系统后直接在’软件和更新->附加驱动’直接选择470(proprietary…

工程化实践:工程配置化设计

文内项目 Github&#xff1a;XIAOJUSURVEY 配置化是很灵活且很常见的使用&#xff0c;那XIAOJUSURVEY里有哪些地方应用到了呢&#xff1f; 基础模板​ 问卷模板​ 在创建问卷时&#xff0c;我们提供了多种问卷类型选择&#xff0c;例如普通问卷、投票、报名、NPS等。 为了实…

安卓碎片Fragment

文章目录 Fragment的简单用法动态添加Fragment在Fragment中实现返回栈碎片与活动之间的通信 Fragment是一种可以嵌入在Activity当中的UI片段&#xff0c;它能让程序更加合理和充分地利用大屏幕的空间。 Fragment的简单用法 先建一个空的项目&#xff0c;然后创建两个fragment文…

单片机振荡电路晶振不起振原因分析与解决方法

晶发电子专注17年晶振生产,晶振产品包括石英晶体谐振器、振荡器、贴片晶振、32.768Khz时钟晶振、有源晶振、无源晶振等&#xff0c;产品性能稳定,品质过硬,价格好,交期快.国产晶振品牌您值得信赖的晶振供应商。 晶振在单片机系统中扮演着至关重要的角色&#xff0c;它为单片机提…

大模型分不清 9.9 与 9.11 谁大,那 Embedding 模型呢?

这是我今天在维也纳举行的 ICML 会议上被问到的问题。 在茶歇期间&#xff0c;一位 Jina 用户向我提出了一个源自 LLM 社区最近讨论的问题。他问我们 Jina Embeddings 能不能判断 9.11 比 9.9 更小&#xff0c;很多大模型在这个小问题上翻了车。 我说&#xff1a;“老实说&am…

Flink 实时数仓(七)【DWS 层搭建(一)流量域汇总表创建】

前言 今天开始 DWS 层的搭建&#xff0c;不知不觉又是周一&#xff0c;都忘了昨天是周末&#xff0c;近两年对我来说&#xff0c;周六日晚上八九点能打一小会篮球就算一周的休息了。不得不说自己真的是天生打工体质&#xff0c;每天不管多累&#xff0c;晚上十二点睡&#xff0…

SpringBoot自动配置和执行过程

Spring的执行流程 1. 加载容器&#xff08;加载配置文件&#xff09; 2. 根据配置完成Bean的初始化&#xff08;扫描配置范围内的五大类注解&#xff09; 3. 将被五大类注解修饰的类注册到Spring容器中 (将对象交给Spring IoC容器管理) 4.注入Bean对象&#xff08;Autowired、R…

Linux - - - Linux 添加环境变量

1.添加环境变量 编辑环境变量配置文件。 vim /etc/profile在最后面新增一行&#xff0c;导出 PATH 变量并在之前的 PATH 变量后面添加冒号&#xff0c;然后添加上你的可执行文件的目录。 export PATH$PATH:/usr/local/aspnetcore/aspnetcore-runtime-8.0.7-linux-x64/2.加载…

GD32 SPI 通信协议

1.0 SPI 简介 SPI是一种串行通信接口&#xff0c;相对于IIC而言SPI需要的信号线的个数多一点&#xff0c;时钟的信号是主机产生的。 MOSI&#xff1a;主机发送&#xff0c;从机接收 MISO&#xff1a;主机接收&#xff0c;从机发送 CS&#xff1a;表示的是片选信号 都是单向…

在线Banner设计工具大比拼:谁更胜一筹

在数字营销的时代&#xff0c;一个吸引眼球的 Banner 广告是吸引潜在客户、提高品牌知名度的关键。为了帮助营销人员和设计师快速创建专业的 Banner 广告&#xff0c;市面上出现了多种易于使用的 Banner 设计工具。本文将介绍几个受欢迎的 Banner 设计工具&#xff0c;包括即时…

路径规划——A*算法

路径规划——A*算法 算法原理 为了解决Dijkstra算法效率低的问题&#xff0c;A Star 算法作为一种启发式算法被提出。该算法在广度优先的基础上加入了一个估价函数。如BFS和常规方法如Dijsktra算法结合在一起的算法&#xff0c;有点不同的是&#xff0c;类似BFS的启发式经常给…

RGB红绿灯——Arduino

光的三原色 牛顿发现光的色散奥秘之后&#xff0c;进一步计算发现&#xff1a;七种色光中只有红、绿、蓝三种色光无法被分解&#xff0c;而其他四种颜色的光均可由这三种色光以不同比例相合而成。于是红、绿、蓝被称为“三原色光”或“光的三原色”。后经证实&#xff1a;红、绿…

提升C++开发效率的利器:深入解析Clang Power Tools

目录 一、引言 二、Clang Power Tools 简介 什么是 Clang Power Tools&#xff1f; 背景与发展历史 与 Clang 编译器的关系 主要开发团队和社区支持 系统要求 安装步骤 基本配置和使用 三、主要功能 代码格式化&#xff08;Clang-Format&#xff09; 代码质量提升 …

springboot+Loki+Loki4j+Grafana搭建轻量级日志系统

文章目录 前言一、日志组件介绍1.1 Loki组件1.2 Loki4j组件1.3 Grafana 二、组件下载安装运行Loki下载安装运行Grafana下载安装运行 三、创建springboot项目总结 前言 日志在任何一个web应用中都是不可忽视的存在&#xff0c;它已经成为大部分系统的标准组成部分。搭建日志可视…