当前位置: 首页 > news >正文

继续优化基于树状数组的cuda前缀和

在之前的博客《借助树状数组的思想实现cuda版前缀和》中,我们用三个kernel实现了基于树状数组的cuda版前缀和,但是在数据量较大时速度不如传统的reduce-then-scan方法,主要原因在于跨block的reduce阶段没有充分利用所有的cuda核心。在本博客中,我们尝试进一步优化,将三个kernel减少到两个kernel,并在跨block的reduce阶段尝试使用更多核心来提升性能。
基于树状数组的方法源于传统方法在reduce阶段其实就是构造了一个完备的树状数组,所以我们可以将scan阶段改成基于树状数组查询的方式,从而达到简化代码的目的。

1. Reduce阶段

Reduce阶段完成两个工作:1. block内构造树状数组;2.block之间构造树状数组。Block之内构造树状数组如下图所示,首先步幅为1的两个元素相加,然后步幅为2的两个元素相加,之后是步幅为4、8、16…直到步幅为block_size/2的两个元素相加。最终的结果正好和树状数组的定义一模一样。
在这里插入图片描述
由于cuda编程的特殊性,比较难以实现跨block之间的同步,而为了构造完整的树状数组,我们必须要访问不同block之间的元素。在之前的方案中,我们只启动了一个block来实现跨block的树状数组构造,这样就严重限制了该步骤的并行性。事实上,我们还可以继续借助block之内的树状数组构造来完成block之间的树状数组构造。区别就在于block之内构造树状数组的步幅是1,而block之间的构造步幅不再是1,所以我们只需要传递一个步幅就可以将block之内和block之间的树状数组构造统一起来。按照该思路实现的reduce代码如下:

__global__ static void gen_bit_in_one_block(int *input, long long n, long long offset) {int tid = blockIdx.x * blockDim.x + threadIdx.x;long long pos = (tid + 1) * offset - 1;int id = threadIdx.x;__shared__ int reduced_sum[512];reduced_sum[id] = input[pos];__syncthreads();int offset = 2;
#pragma unroll 9while(offset <= 512) {if(((tid + 1) & (offset - 1)) == 0) {reduced_sum[id] += reduced_sum[id - offset / 2];}__syncthreads();offset <<= 1;}input[pos] = reduced_sum[id];}
}

上面的代码和之前的实现基本一致,主要是增加了一个offset变量用来获取正确的初始步幅。此外,为了加速显存访问,我们引入了共享内存,构造树状数组的过程都在共享内存中完成。
上述代码在构造一个warp内的树状数组时,我们还可以使用__shfl_up_sync来加速,但是收益不明显,感兴趣的可以继续尝试优化。

2. Scan阶段

构造好完整的树状数组之后,我们就可以利用其查询每个位置的前缀和了,代码和之前一样:

__global__ static void calc_sum_using_bit(int *input, int *output, int n) {int tid = blockIdx.x * blockDim.x + threadIdx.x;if (tid < n) {int sum = input[tid];int idx = tid + 1;idx -= (idx & -idx);while (idx > 0) {sum += input[idx - 1];idx -= (idx & -idx);}output[tid] = sum;}
}

不过经过仔细分析我们就会发现上面的代码实现不是work-efficient的,我们详细解释一下原因。CPU串行实现计算前缀和只需要n次加法。Reduce阶段总共也只会有n个线程参与求和:n/2 + n/4 + n/8 + … = n,所以reduce阶段是work-efficient的。但是scan阶段,参与运算的线程总数为从1到n的二进制中1的个数,而这个数值是n*logn量级的,所以虽然整体复杂度是O(logn)的,但是参与运算的线程数是O(nlogn)的,所以就不属于work-efficient的实现。

3. 完整调用逻辑

我们借助上面两个kernel实现完整的前缀和计算。代码如下:

void calc_prefix_sum(int *input, int *output, int n) {int *buffer1, *buffer2;cudaMalloc(&buffer1, n * sizeof(int));cudaMalloc(&buffer2, n * sizeof(int));cudaMemcpy(buffer1, input, n * sizeof(int), cudaMemcpyHostToDevice);long long offset = 1;long long count = n;dim3 dimBlock(512);do {dim3 dimGrid(get_block_size(count, 512));gen_bit_in_one_block<<<dimGrid, dimBlock>>>(buffer1, count, offset);count /= 512;offset *= 512;} while(offset < n);dim3 dimGrid2(get_block_size(n, 512));calc_sum_using_bit<<<dimGrid, dimBlock2>>>(buffer1, buffer2, n);cudaMemcpy(output, buffer2, n * sizeof(int), cudaMemcpyDeviceToHost);cudaFree(buffer1);cudaFree(buffer2);
}

相比之前的实现,我们需要循环调用gen_bit_in_one_block来构造完整的树状数组。所以,虽然只有两个kernel,但是调用kernel的次数不止两次。

4. 性能对比

我们用RTX4090来验证性能。这次我们引入cub实现,号称最快的前缀和实现,然后再加上传统的reduce-then-scan实现,看看三种不同实现在不同长度下的性能如何,对比如下(单位毫秒):

长度10010001万10万100万1000万1亿10亿
BIT0.190.190.190.190.220.392.1119.46
BCAO0.020.020.030.030.080.312.2319.59
CUB0.030.030.030.040.040.070.878.69

可以看出,基于树状数组的实现在长度较长时可以与BCAO类似,但是两者都远差于CUB的实现。CUB的实现基于论文《Single-pass Parallel Prefix Scan with Decoupled Look-back》实现,只需要一个kernel一次遍历即可完成前缀和计算,但是这篇论文较难读懂暂时不理解详细的原理,待后续研究明白再仿照其进行实现。用树状数组实现的好处是代码较为简洁,速度也还凑合,可以作为面试中的实现来学习。


文章转载自:

http://SgqWHuHC.wzwyz.cn
http://uQZoqi7O.wzwyz.cn
http://C5vbiDer.wzwyz.cn
http://3uPHmXit.wzwyz.cn
http://70kGwueq.wzwyz.cn
http://emXDKlFp.wzwyz.cn
http://XsJGUjEI.wzwyz.cn
http://JHbodKOV.wzwyz.cn
http://QmrsqKU5.wzwyz.cn
http://uK6JGwHN.wzwyz.cn
http://KGnhvjB9.wzwyz.cn
http://Zz9raPHe.wzwyz.cn
http://73aJg8IK.wzwyz.cn
http://ruAAUjc4.wzwyz.cn
http://Z2RdM7Ea.wzwyz.cn
http://nXKF3F4Y.wzwyz.cn
http://xdLylWnu.wzwyz.cn
http://Vao77E9U.wzwyz.cn
http://AipaUA9Q.wzwyz.cn
http://erBeM1Ct.wzwyz.cn
http://EdebTlHV.wzwyz.cn
http://fjNFqAoI.wzwyz.cn
http://bcOR46I2.wzwyz.cn
http://SRNvt2QE.wzwyz.cn
http://cbGLbRoi.wzwyz.cn
http://0IK2DKDj.wzwyz.cn
http://e3WLY4WB.wzwyz.cn
http://z4igeSGR.wzwyz.cn
http://GlfCdUn5.wzwyz.cn
http://CcsGMraM.wzwyz.cn
http://www.dtcms.com/a/372358.html

相关文章:

  • 数组常见算法
  • 数仓建模理论
  • 致远A8V5 9.0授权文件
  • 【New Phytologist】​​单细胞多组学揭示根毛对盐胁迫的特异性响应文献分享
  • MyBatis 拦截器让搞定监控、脱敏和权限控制
  • 20250907-0101:LangChain 核心价值补充
  • 论CMD、.NET、PowerShell、cmdlet四者关系
  • 从IFA展会看MOVA的“全维进阶”如何重新定义智能家居边界
  • SpringBoot 数据脱敏实战: 构建企业级敏感信息保护体系
  • 公链分析报告 - 模块化区块链1
  • 20250907-01:理解 LangChain 是什么 为什么诞生
  • 做一个鉴权系统
  • Javaweb - 14.5 Vue3 路由机制
  • 2.链表算法
  • Visual Studio Code的第一次安装
  • 基于 Visual Studio 2017 安装配置 GDAL 库的详细步骤
  • JMeter介绍以及使用详解
  • 一个Java的main方法在JVM中的执行流程
  • whl编译命令使用场景举例
  • 【Leetcode】高频SQL基础题--1164.指定日期的产品价格
  • 力扣1210. 穿过迷宫的最少移动次数 详解
  • Redis 从入门到精通:全平台安装与性能优化配置指南
  • RestClient查询和数据聚合
  • 前后端中的回调机制:含义、作用与实现详解
  • 四、神经网络的学习(下)
  • 万字详解网络编程之socket
  • PNG和JPEG和BMP文件格式转换
  • 语音之战+通用大模型,AI霸权决战打响
  • eslint 和 prettier 的相同点和区别
  • 苹果 FoundationModels 秘典侠客行:隐私为先的端侧 AI 江湖