并行编程实战——CUDA编程的并行前缀和

一、前缀和

Prefix Sum,前缀和,也叫做扫描。它是一种应用在并行计算中的基础的算法。有点类似于一些数字处理的游戏。说白了就是从给定的输入序列中获取指定方式的累计操作结果的一种方法。这种操作方式可以是和也可以是差、积等。

这样说可能不太好理解,看一个前缀和(累加)简单的例子就明白了:

输入序列:1 2 3 4 5 6

输出序列:1 3 6 10 15 21

这种算法分为两种情况即(以和为例):

包含扫描(inclusive scan):如果输入序列定义为An,输出序列定义为Bn,则Bn[i] = An[0]+...+An[i];

排除扫描(exclusive scan):定义同上,则Bn[i] = An[0]+...+An[i-1];

它们的区别其实很明白,就是在计算和时是否包含自身。

二、前缀和的实现

如果单纯的只是实现上述的目标,这个就比较简单了,遍历进行处理即可。但这种情况下,如果数据量小还可以忍受,但一旦数据量大了,遍历的过程是十分漫长的,效率低的可怕。比如第一个数可能要遍历N次。算法的整体时间复杂度为O(n)。而且其无法进行并行化。但既然使用GPU,如果不发挥其并行化的优势,还有啥意义呢?

而实际上,CUDA天然的多个核心可以被有效利用起来,只要能提供良好的算法处理,就可以发挥出这些多核心的优势,迅速的达到与遍历迭代相同的结果。但运行的效率却大幅的提高。

三、主要算法

前缀和的算法中,可以发现其影响效率的主要原因在于数据遍历和结果的移动(赋值的变化),是不是发现这种场景有点熟悉,是不是有点类似于分布式中的map reduce?是不是类似于算法中的分治法?

明白了这些,再加上知道有两种前缀和的计算方法,就可以知道其分别对应算法的基本解决之道。

  1. hillis-steele算法

    它是一种inclusive scan算法,其主要的思想就是分治加传播,即scan-then-propagate。即把每个元素与左边相邻的第一个元素相加,得到一组新的数据;然后再将每个元素与左边相邻的第2个元素相加,如此反复迭代,直接完成。最后即可结果。其算法实时的时间复杂度为O(log n),工作复杂度为O(n log n),空间复杂度为O(n)

  2. Belloch算法

    它属于exclusive scan算法,它分为两步即up-sweep(reduce)与down-sweep两个部分。即先在up-sweep阶段通过递归方法计算所有元素的总和并完成偶数位的初步刷新,然后再通过换位和递归加和的方法计算偶数位和余下的奇数位的结果,最终完成相关的计算。其算法实时的时间复杂度为O(2 log n),工作复杂度和空间复杂度为O(n)

这两种算法中,hillis-steele算法并行度要比Belloch算法高,前者更适合于中小数据量而后者则更适合于大数据量。从这一点上看,是不是可以利用分治的思想把它们整合起来,线程块内使用Belloch算法而块间则使用hillis-steele算法。

四、应用场景

一般来说,对性能要求高,实现复杂度低且GPU计算资源富裕,数据规模小时建议使用hillis-steele算法,如实时的图形渲染、信号处理以及深度学习中的推理等等。

而当要求费效比、数据规模大且内存带宽紧张时,建议使用Belloch算法。如大数据计算、大规模的科学计算以及稀疏矩阵处理等。

当然,很多情况不是一种"手术刀"可以解决的,在一些具体的环境,如医疗影像处理以及自动驾驶相关的场景中(如图像的分割、压缩、扫描等),可能单一的一种算法无法解决问题,就得灵活的适配两种方法一起协同工作。

五、例程

下面看一个简单的例程:

c 复制代码
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>

// Hillis-Steele
__global__ void prefixSum(int* input, int* output, int n) {
    extern __shared__ int tmp[];

    int tid = threadIdx.x;
    int idx = blockIdx.x * blockDim.x + threadIdx.x;

	// copy to shared memory
    if (idx < n) {
        tmp[tid] = input[idx];
    }
    else {
        tmp[tid] = 0;
    }
    __syncthreads();

	// prefix sum
    for (int step = 1; step < blockDim.x; step *= 2) {
        if (tid >= step) {
            tmp[tid] += tmp[tid - step];
        }
        __syncthreads();
    }

    if (idx < n) {
        output[idx] = tmp[tid];
    }
}

void checkPrefixSum(int* input, int* output, int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += input[i];
        if (output[i] != sum) {
            printf("is err! index %d: GPU=%d, CPU=%d\n", i, output[i], sum);
            return;
        }
    }
    printf("is ok!\n");
}

int main() {
    const int N = 16;  // It must be a power of 2 and less than the block size
	const int blockSize = 256;  // thread per block

    int* inputData = (int*)malloc(N * sizeof(int));
    int* outputData = (int*)malloc(N * sizeof(int));

    // init
    printf("init array: ");
    for (int i = 0; i < N; i++) {
        inputData[i] = i + 1;  // [1, 2, 3, ..., N]
        printf("%d ", inputData[i]);
    }
    printf("\n");

    int* dInput, * dOutput;
    cudaMalloc(&dInput, N * sizeof(int));
    cudaMalloc(&dOutput, N * sizeof(int));

	// host to device
    cudaMemcpy(dInput, inputData, N * sizeof(int), cudaMemcpyHostToDevice);

    dim3 block(blockSize);
    dim3 grid((N + blockSize - 1) / blockSize);

    prefixSum << <grid, block, blockSize * sizeof(int) >> > (dInput, dOutput, N);

	// device to host
    cudaMemcpy(outputData, dOutput, N * sizeof(int), cudaMemcpyDeviceToHost);

    printf("prefix result: ");
    for (int i = 0; i < N; i++) {
        printf("%d ", outputData[i]);
    }
    printf("\n");

    // check
    checkPrefixSum(inputData, outputData, N);

	// cleanup
    free(inputData);
    free(outputData);
    cudaFree(dInput);
    cudaFree(dOutput);

    return 0;
}

它的运行结果是:

复制代码
init array: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
prefix result: 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136
is ok!

六、总结

本文重点不是在分析具体的算法上,这两上算法会在后面进行更全面的分析和说明。GPU能够在近几年迅速发展壮大到CPU都要让位的可能不是没有原因的。AI的迅速发展,导致了并行计算量海量的暴增,这才GPU发展的底气。

相关推荐
maplewen.1 小时前
C++11 返回值优化
开发语言·c++·面试
sTone873752 小时前
std::functional 使用场景
c++
hetao17338372 小时前
2026-02-09~02-12 hetao1733837 的刷题记录
c++·算法
ADDDDDD_Trouvaille2 小时前
2026.2.12——OJ72-74题
c++·算法
小米的修行之路2 小时前
NVIDI核心板cuda, cudnn,tensorrt安装方法
nvidia·tensorrt·cuda·cudnn
梵刹古音2 小时前
【C++】函数重写
开发语言·c++
Titan20242 小时前
C++异常学习笔记
c++·笔记·学习
柒儿吖3 小时前
DDlog 高性能异步日志库在 OpenHarmony 的 lycium 适配与分步测试
c++·c#·openharmony
民国二十三画生3 小时前
C++(兼容 C 语言) 的标准输入语法,用来读取一行文本
c语言·开发语言·c++