CUDA并行归约算法(二)

news2024/11/25 0:53:41

CUDA并行归约算法(二)

文章目录

  • CUDA并行归约算法(二)
    • 前情回顾
    • 线程束分化
    • 内存组织
    • Reference
      • >>>>> 欢迎关注公众号【三戒纪元】 <<<<<

前情回顾

首先看下上节设计的核函数,如何进行并行归约算法的:

__global__ void ReduceNeighbour(int* g_idata, int* g_odata, unsigned int n)
{
    //set thread ID
    unsigned int t_id = threadIdx.x;
    // boundary check
    if (t_id >= n)
    {
        return;
    }

    int *idata = g_idata + blockIdx.x * blockDim.x;
    // in-place reduction in global memory
    for (int stride = 1; stride < blockDim.x; stride *= 2) {
        if((t_id % (2 * stride)) == 0) {
            idata[t_id] += idata[t_id + stride];
        }
        // synchronize within block
        __syncthreads();
    }

    // write result for this block to global memory
    if (t_id == 0)
    {
        g_odata[blockIdx.x] = idata[0]; // 记录每个block的值
    }
}

主要是通过设计stride,使其每次加2

idata[t_id] += idata[t_id + stride];

控制每次迭代的与当前线程相加的数,t_id 代表当前线程ID,t_id + stride 代表被加线程ID

这就造成了每轮迭代只有部分线程是活跃的,越到后面,不活跃的线程越多。

由于GPU的硬件设计,每次调度都会以1个线程束为单位进行,所以,1个线程束里只要有1个线程需要活跃,当前线程束内的线程全部都会活跃起来,即便很多线程不参与计算,这就非常影响程序的执行效率。

可以看出有2个比较明显的优化点:

  • 线程束分化
  • 内存访问

线程束分化

可以通过重新组织线程索引来解决线程束分化问题:

__global__ void ReduceNeighboredLess(int *g_idata, int *g_odata, unsigned int n) {
    unsigned idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx > n) {
        return;
    }
    
    unsigned int tid = threadIdx.x;
    // convert global data pointer to the local point of this block
    int* idata = g_idata + blockIdx.x * blockDim.x;
    // in-place reuction in global memory
    for (int stride = 1; stride < blockDim.x; stride *= 2) {
        // convert tid into local array index
        int index = 2 * stride * tid;
        if (index < blockDim.x) {
            idata[index] += idata[index + stride];
        }
        __syncthreads();
    }

    // write result for this block to global memory
    if (tid == 0) {
        g_odata[blockIdx.x] = idata[0];
    }
}

先跑下结果:

Using device 0: NVIDIA GeForce RTX 3070 Laptop GPU
   array size: 16777216
grid size: 16384, block size: 1024
CPU sum: 2139035173
CPU reduction elapsed 48.8141 ms, CPU sum: 2139035173
gpu sum:2139035173 
gpu ReduceNeighboredLess elapsed 1.270056 ms     <<<grid 16384 block 1024>>>
Test success!

优化前GPU时间为 2.512932 ms,优化后的时间为 1.270056 ms,节约了近一半的时间。

因此,避免线程束的分化十分重要

到底怎么做到的呢?

来看下线程结构

总共有16384个grid,在每个线程块(block)有1024个线程( 16777216 = 16384 × 1024 16777216 = 16384 \times 1024 16777216=16384×1024),每个block中又包含32个双线程束,也就是1024( 1024 = 32 × 32 1024 = 32 \times 32 1024=32×32)个线程被32个线程束管理着,每个线程束管理32个线程。

每次参与计算的线程号其实就是 int index = 2 * stride * tid;index + stride,而线程号需要满足index < blockDim.x (1024)的条件,

因此,第一轮stride = 1,实际参与计算的线程号为:0,1,2,3, …, 511,512。而 512 = 32 × 16 512 = 32 \times 16 512=32×16,也就是实际参与计算的也就前16个线程束,后16个线程束在if (index < blockDim.x)就结束了。

第二轮,stride = 2,实际参与计算的线程id 为 0,2,4,6,12, 14, 256。也就是前8个线程束参与了计算,后24个线程束不计算。

而在原来的代码中,第一轮是线程id为偶数的线程参与计算,第二轮是线程id是4的倍数的线程参与计算,但是其他线程仍然是活跃的。

内存组织

之前的方法,第一轮过后,会造成第二轮因为使用了stride作为跨度量而导致的内存访问不连续。

因此需要重新组织一下配对方法,让对内存的访问更加集中。

可以使用交错配对方法:

核函数如下:

__global__ void ReduceInterleaved(int * g_idata, int *g_odata, unsigned int n)
{
    unsigned idx = blockIdx.x*blockDim.x + threadIdx.x;
    if (idx >= n)
        return;
    // convert global data pointer to the local point of this block
    int *idata = g_idata + blockIdx.x*blockDim.x;
    unsigned int tid = threadIdx.x;
    //in-place reduction in global memory
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1)
    {

        if (tid <stride)
        {
            idata[tid] += idata[tid + stride];
        }
        __syncthreads();
    }
    //write result for this block to global men
    if (tid == 0)
        g_odata[blockIdx.x] = idata[0];
}

结果为:

Using device 0: NVIDIA GeForce RTX 3070 Laptop GPU
        with array size 16777216  grid 16384 block 1024 
cpu sum:2139617404 
cpu reduction elapsed 56.808949 ms cpu_sum: 2139617404
gpu sum:2139617404 
gpu reduceInterleaved elapsed 1.042843 ms     <<<grid 16384 block 1024>>>
Test success!

优化线程束分化后,又改进了内存放访问方式,时间消耗(1.042843 ms)比仅改进线程束时间消耗更短。

因此,对全局内存的访问要尽量进行合并访问与存储

Reference

  • CUDA编程入门(五)更高效的并行归约算法

>>>>> 欢迎关注公众号【三戒纪元】 <<<<<

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

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

相关文章

基于FPGA的视频接口之HDMI1.4(以下)编码

简介 为什么要特别说明HDMI的版本,是因为HDMI的版本众多,代表的HDMI速度同样不同,当前版本在HDMI2.1速度达到48Gbps,可以传输4K及以上图像,但我们当前还停留在1080P@60部分,且使用的芯片和硬件结构有很大差别,故将HDMI分为两个部分说明1080@60以下分辨率和4K以上分辨率(…

docker运行redis容器

参考文章 Redis从入门到精通&#xff08;4&#xff09;&#xff1a;docker运行redis容器详解 问题及总结 docker 命令 拉取镜像&#xff1a;docker pull redis 或者 docker pull redis:bullseye&#xff1b;查看镜像&#xff1a;docker image ls&#xff1b;直接运行 redis…

Flask SQLAlchemy_Serializer ORM模型序列化

在前后端分离项目中&#xff0c;经常需要把ORM模型转化为字典&#xff0c;再将字典转化为JSON格式的字符串。在遇到sqlalchemy_serializer之前&#xff0c;我都是通过类似Java中的反射原理&#xff0c;获取当前ORM模型的所有字段&#xff0c;然后写一个to_dict方法来将字段以及…

Golang假共享(false sharing)详解

多核处理器(SMP)系统中, 每一个处理器都有一个本地高速缓存。内存系统必须保证高速缓存的一致性。当不同处理器上的线程修改驻留再同一高速缓存中的变量时就会发生假共享(false sharing),结果导致高速缓存无效,并强制更新,进而影响系统性能。 什么是假共享(false sharin…

论文解读|VoxelNet:基于点云的3D物体检测的端到端学习

原创 | 文 BFT机器人 01 摘要 论文提出了表述了一个新的基于点云的3D检测方法&#xff0c;名为VoxelNet&#xff0c;该方法是一个端到端可训练的深度学习架构&#xff0c;利用了稀疏点云的结构特性&#xff0c;直接在稀疏的3D点上进行操作&#xff0c;并通过高效的并行处理体素…

Stable Diffusion - ChatGPT4 与 Stable Diffusion 结合提供无限创意构图

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131782672 ChatGPT 和 StableDiffusion 结合使用的优势&#xff1a; 高效率&#xff1a;ChatGPT 可以在很短的时间内完成复杂的语言任务&#xf…

Kotlin基础(六) 枚举类和扩展

前言 本文主要讲解kotlin枚举类和扩展 Kotlin文章列表 Kotlin文章列表: 点击此处跳转查看 目录 1.1 枚举类 1.1.1 枚举类的基本用法 Kotlin中的枚举类&#xff08;enum class&#xff09;用于定义一组具有预定义值的常量。它们在许多情况下都很有用&#xff0c;例如表示一组…

介绍性能压力测试的重要性

在当今数字化时代&#xff0c;软件和应用程序的性能对于用户体验和业务成功至关重要。为了确保系统在面临高负载和压力时能够正常运行&#xff0c;性能压力测试成为一项不可或缺的活动。本文将介绍性能压力测试的重要性。 性能压力测试是一种通过模拟实际场景中的负荷和用户访问…

Echarts中饼状图,图例显示value而不是name

直接上代码 formatter(name) {var tarValue;for (var i 0; i < data.length; i) {if (data[i].name name) {tarValue data[i].value;}}var v tarValue;return [tarValue]} 效果图

spring boot 多模块项目搭建Knife4j文档,swagger-ui x2

介绍: knife4j jeecg-boot用的就是这个&#xff0c;我之前要搭过swagger-ui&#xff0c;但外观&#xff0c;体验都没有knife4j好&#xff0c;我没记错的话已经停止发布版本了&#xff0c;所以我的多模块项目就用到了这个&#xff0c;还搭建了jwt token获取我也是在网上找的…

260道2023最新网络安全工程师面试题(附答案)

2023年过去了一大半&#xff0c;先来灵魂三连问&#xff0c;年初定的目标完成多少了&#xff1f;薪资涨了吗&#xff1f;女朋友找到了吗&#xff1f; ​好了&#xff0c;不扎大家的心了&#xff0c;接下来进入正文。 由于我之前写了不少网络安全技术相关的文章和回答&#xff…

pdf文件如何生成长图?分享两个免费的方法给大家!

在某些情况下&#xff0c;我们可能需要将长图转换为PDF文件&#xff0c;以便更方便地分享和存档。本文将介绍两种免费的方法&#xff0c;帮助您实现这一目标。方法一是使用记灵在线工具的PDF转长图功能&#xff0c;方法二是利用PDF24工具进行转换。让我们一起来了解具体操作步骤…

blender 叶片制作

圆润叶片 效果展示 shift a 新建矩形&#xff0c;s y 延 y 轴方向压扁&#xff0c;ctrl r 循环切割&#xff0c;滚动滚轮&#xff0c;延 y 轴方向切两条循环线&#xff0c;框选点&#xff0c;s 缩放&#xff0c;调整到叶片造型&#xff0c;添加细分修改器&#xff1b;给叶片…

构建数据中台的三要素:方法论、组织和技术

知道要转型&#xff0c;要建设数据中台&#xff0c;却不知咋做&#xff0c;咋办&#xff1f; 现在有很多讲“如何建设数据中台”文章&#xff0c;观点各不相同&#xff1a; 数据中台是数据建设方法论&#xff0c;按照数据中台设计方法和规范实施就可建成数据中台数据中台背后…

WPF嵌入外部exe应用程序-去除子窗体边框样式

WPF嵌入外部exe应用程序-去除子窗体边框样式 去除子窗体边框样式导入winodows API使用API去除边框报错&#xff1a;解决实现效果 完整实现代码 接着上一篇WPF嵌入外部exe应用程序-实现基本的嵌入&#xff0c;解决子窗体边框样式问题,去掉子窗体样式&#xff0c;让其融为一体&am…

家政上门小程序|同城家政预约上门小程序开发|上门家政软件源码

随着生活水平的提高&#xff0c;越来越多的人开始借助家政服务来解决日常生活中的琐事。为了方便用户寻找和预约家政服务&#xff0c;家政上门小程序应运而生。家政上门小程序开发具有多种功能&#xff0c;使其成为家政服务行业的重要工具。本文将介绍一些家政上门小程序开发的…

品牌营销策略:如何有效打造品牌知名度与口碑?

品牌营销策略是企业在市场竞争中脱颖而出的重要手段&#xff0c;它能够帮助企业树立品牌形象&#xff0c;提升品牌知名度&#xff0c;增强品牌影响力&#xff0c;从而获得更多的市场份额和利润。那么&#xff0c;如何制定一套有效的品牌营销策略呢&#xff1f;以下是一秒推小编…

《向量数据库指南》:向量数据库Pinecone管理索引教程(一)

在本节中,我们将说明如何获取索引列表、创建索引、删除索引和描述索引。 要了解与索引相关的概念,请参见索引。 ⚠️警告 Starter(免费)计划上的索引将在7天的不活动后被删除。为了 防止这种情况,请发送任何API请求或登录控制台。这将计算为 活动。 获取有关您的索…

解决appium-doctor报 mjpeg-consumer cannot be found

解决appium-doctor报 mjpeg-consumer cannot be found npm i -g mjpeg-consumer

深入开箱跑分全志A523平板电脑 台电P26T

首先外观和观感就不说了&#xff0c;图都有&#xff0c;来看看内部实际的东西。 主控全志A523M00X0000&#xff0c;配套Android 13 5.15 Kernel系统。4G内存&#xff0c;64G eMMC&#xff0c;屏幕1280*800分辨率。 平板开启了安全启动所以想买来开发刷机可以歇歇了 &#xf…