CUDA高性能计算经典问题②:前缀和

本文深入探讨CUDA中的前缀和(Prefix Sum)问题,通过多个步骤逐步优化,从ScanThenFan到ReduceThenScan,利用Shared Memory、Warp Shuffle等技术,将时间从72390us降低至2467us,详细解析了CUDA并行计算的挑战和解决方案。

75ee6ddf978a022e33c626cf1ec80ff9.png

撰文 | Will Zhang

上一篇《CUDA高性能计算经典问题①:归约》中,我们讨论了CUDA中如何实现高效Reduction,这次来讨论下一个经典问题Prefix Sum(前缀和),也被称为Scan/Prefix Scan等。Scan 是诸如排序等重要问题的子问题,所以基本是进阶必学问题之一。

1

问题定义

首先我们不严谨地定义这个问题,输入一个数组input[n],计算新数组output[n], 使得对于任意元素output[i]都满足:

output[i] = input[0] + input[1] + ... input[i]

一个示例如下:

50d284776a9451575b56f49b0ecc12ad.png

如果在CPU上我们可以简单地如下实现:

void PrefixSum(const int32_t* input, size_t n, int32_t* output) {
  int32_t sum = 0;
  for (size_t i = 0; i < n; ++i) {
    sum += input[i];
    output[i] = sum;
  }
}

问题来了,如何并行?而且是几千个线程和谐地并行?这个问题里还有个明显的依赖,每个元素的计算都依赖之前的值。所以第一次看到这个问题的同学可能会觉得,这怎么可能并行?

而更进一步地,如何用CUDA并行,Warp级别怎么并行,Shared Memory能装下数据的情况怎么并行,Shared Memory装不下的情况如何并行等等。

2

ScanThenFan

首先我们假设所有数据都可以存储到Global Memory中,因为更多的数据,核心逻辑也是类似的。

我们介绍的第一个方法称为ScanThenFan,也很符合直觉,如下:

  • 将存储在Global Memory中的数据分为多个Parts,每个Part由一个Thread Block单独做内部的Scan,并将该Part的内部Sum存储到Global Memory中的PartSum数组中

  • 对这个PartSum数组做Scan,我们使用BaseSum标识这个Scan后的数组

  • 每个Part的每个元素都加上对应的BaseSum

如下图

8625861fea99ec206a128c93262d727e.png

3

Baseline

我们先不关注Block内如何Scan,在Block内先使用简单的单个线程处理,得到如下代码:

__global__ void ScanAndWritePartSumKernel(const int32_t* input, int32_t* part,                                                                                                                                                                                                 int32_t* output, size_t n,
                                          size_t part_num) {
  for (size_t part_i = blockIdx.x; part_i < part_num; part_i += gridDim.x) {
    // this part process input[part_begin:part_end]
    // store sum to part[part_i], output[part_begin:part_end]
    size_t part_begin = part_i * blockDim.x;
    size_t part_end = min((part_i + 1) * blockDim.x, n);
    if (threadIdx.x == 0) {  // naive implemention
      int32_t acc = 0;
      for (size_t i = part_begin; i < part_end; ++i) {
        acc += input[i];
        output[i] = acc;
      }
      part[part_i] = acc;
    }
  }
}
__global__ void ScanPartSumKernel(int32_t* part, size_t part_num) {
  int32_t acc = 0;
  for (size_t i = 0; i < part_num; ++i) {
    acc += part[i];
    part[i] = acc;
  }
}
__global__ void AddBaseSumKernel(int32_t* part, int32_t* output, size_t n,
                                 size_t part_num) {
  for (size_t part_i = blockIdx.x; part_i < part_num; part_i += gridDim.x) {
    if (part_i == 0) {
      continue;
    }
    int32_t index = part_i * blockDim.x + threadIdx.x;
    if (index < n) {
      output[index] += part[part_i - 1];
    }
  }
}
// for i in range(n):
//   output[i] = input[0] + input[1] + ... + input[i]
void ScanThenFan(const int32_t* input, int32_t* buffer, int32_t* output,
                 size_t n) {
  size_t part_size = 1024;  // tuned
  size_t part_num = (n + part_size - 1) / part_size;
  size_t block_num = std::min<size_t>(part_num, 128);
  // use buffer[0:part_num] to save the metric of part
  int32_t* part = buffer;
  // after following step, part[i] = part_sum[i]
  ScanAndWritePartSumKernel<<<block_num, part_size>>>(input, part, output, n,
                                                      part_num);
  // after following step, part[i] = part_sum[0] + part_sum[1] + ... part_sum[i]
  ScanPartSumKernel<<<1, 1>>>(part, part_num);
  // make final result
  AddBaseSumKernel<<<block_num, part_size>>>(part, output, n, part_num);
}

现在的代码里很多朴素实现,但我们先完成一个大框架,得到此时的耗时72390us作为一个Baseline。

4

Shared Memory

接着,我们看ScanAndWritePartSumKernel函数,我们先做个简单的优化,将单个Part的数据先Load到Shared Memory中再做同样的简单逻辑,如下

__device__ void ScanBlock(int32_t* shm) {
  if (threadIdx.x == 0) {  // naive implemention
    int32_t acc = 0;
    for (size_t i = 0; i < blockDim.x; ++i) {
      acc += shm[i];
      shm[i] = acc;
    }
  }
  __syncthreads();
}
__global__ void ScanAndWritePartSumKernel(const int32_t* input, int32_t* part,
                                          int32_t* output, size_t n,
                                          size_t part_num) {
  extern __shared__ int32_t shm[];
  for (size_t part_i = blockIdx.x; part_i < part_num; part_i += gridDim.x) {
    // store this part input to shm
  
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值