CUDA 编程笔记:使用 CUDA 加速数组总和

发布于:2025-08-16 ⋅ 阅读:(35) ⋅ 点赞:(0)

一、前言

        使用 CUDA 并行计算可以加速数组所有元素之和,由原来的逐一累加,变为多区间并行累加,并将多区间的和再累加,在数组较大时,有明显的加速效果。

        该小结还使用了共享内存,提高访问效率。

二、CPU 计算

        这里直接放源码:

int _sum_cpu(int *ptr, int count)
{
    int sum = 0;
    for (int i = 0; i < count; i++)
    {
        sum += ptr[i];
    }
    return sum;
}

        这是一个非常简单的 for 循环数组求和,CPU 需要依次读取计算,属于单线程依赖计算。如果改用多线程,就需要分区间进行,防止数据竞争和重复计算。

三、GPU 并行计算

        对于较大的数组,会将每个所有元素均分给每个线程。例如,数组总数为 10000000,grid 大小为 65536,也就是线程数,那么每个线程负责 ceil(10000000 / 65536) = 153 个元素的计算。这样做的目的是实现动态数组大小的适配,不依赖线程数。具体做法为:

        每个线程从自己的起始位置开始,以整个网格的线程总数为步长(即65536),跳跃地读取数据并进行累加,核心目的是将超出线程数量的原始数据,压缩到与线程数量相等的局部和数据(10000000 -> 65536),每个线程处理元素数(for循环次数)​​:ceil(10000000 / 65536) = 153。

下面是 CUDA 核函数的源码,有很多注释辅助理解:

__global__ void _sum_gpu(int *input, int count, int *output) {
    __shared__ int sum_per_block[BLOCK_SIZE];

    int temp = 0;
    // 每个线程从自己的起始位置开始,以整个网格的线程总数为步长(即65536),跳跃地读取数据并进行累加
    // 核心目的是将超出线程数量的原始数据,压缩到与线程数量相等的局部和数据(10000000 -> 65536)
    // 每个线程处理元素数(for循环次数)​​:ceil(10000000 / 65536) = 153
    for (int idx = threadIdx.x + blockDim.x * blockIdx.x;
         idx < count;
         idx += gridDim.x * blockDim.x)
    {
        temp += input[idx];
    }

    // 将各线程的局部和存入共享内存
    // ​​这256个block是独立在SM上调度的,每个block中的256个线程共享同一个共享内存空间,但不同block的共享内存是完全隔离的​​
    sum_per_block[threadIdx.x] = temp;
    // 确保当前block中所有线程完成写入后再继续
    __syncthreads();

    //**********shared memory summation stage***********
    // 每次迭代将数据规模减半(类似折半求和)
    for (int length = BLOCK_SIZE / 2; length >= 1; length /= 2)
    {
        int double_kill = -1;
        // 仅低索引线程参与计算(每次减半)
        if (threadIdx.x < length)
        {
            // 例:1 2 3 4,这里将1和3相加,2和4相加
            double_kill = sum_per_block[threadIdx.x] + sum_per_block[threadIdx.x + length];
        }
        // 确保所有线程完成对共享内存的​​读取​​(避免部分线程写入前被读取)
        __syncthreads(); 

        if (threadIdx.x < length)
        {
            // 结果回写到共享内存的前半段(覆盖旧值)
            sum_per_block[threadIdx.x] = double_kill;
        }
        // 确保写入操作完成后再进入下一轮归约(防止脏数据)
        __syncthreads();

    } // the per-block partial sum is sum_per_block[0]

    // 跳过超出数组范围的线程块(如网格过大时),避免无效线程块的冗余原子操作
    if (blockDim.x * blockIdx.x < count)
    {
        // the final reduction performed by atomicAdd()
        // 每个block的 0 号线程将块部分和累加到全局输出
        // atomicAdd确保跨线程块的原子操作
        if (threadIdx.x == 0)
            atomicAdd(output, sum_per_block[0]);
    }
}

需要注意的是,代码中多次调用了线程同步函数:

__syncthreads();

每一步的作用不一样,读者可以结合注释理解。

四、源文件代码

sum.cu

#include<stdio.h>
#include<stdint.h>
#include"error.cuh"
#include<time.h>
#include<stdlib.h>   //for srand()/rand()
#include<sys/time.h> //for gettimeofday()/struct timeval

#define N 10000000
#define BLOCK_SIZE 256
//#define BLOCKS ((N + BLOCK_SIZE - 1) / BLOCK_SIZE) 
#define BLOCKS 256

// CUDA 的统一内存(Unified Memory)是通过 cudaMallocManaged 函数和 __managed__ 关键字实现的主机与设备的透明化内存访问
// 其核心原理是将物理存储位置抽象为统一的虚拟地址空间,当 CPU 或 GPU 访问数据时,系统自动完成数据迁移(按需分页迁移),开发者无需手动调用 cudaMemcpy
// 统一内存消除了通过 cudaMemcpy*() 例程进行显式数据移动的需要,而不会因将所有数据放入零拷贝内存而导致性能损失
__managed__ int source[N];               //input data
__managed__ int final_result[1] = {0};   //scalar output

void _init(int *ptr, int count)
{
    uint32_t seed = (uint32_t)time(NULL); 
    srand(seed);  //reseeding the random generator

    //filling the buffer with random data
    for (int i = 0; i < count; i++) ptr[i] = rand();
}

double get_time()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return ((double)tv.tv_usec * 0.000001 + tv.tv_sec);
}

__global__ void _sum_gpu(int *input, int count, int *output) {
    __shared__ int sum_per_block[BLOCK_SIZE];

    int temp = 0;
    // 每个线程从自己的起始位置开始,以整个网格的线程总数为步长(即65536),跳跃地读取数据并进行累加
    // 核心目的是将超出线程数量的原始数据,压缩到与线程数量相等的局部和数据(10000000 -> 65536)
    // 每个线程处理元素数(for循环次数)​​:ceil(10000000 / 65536) = 153
    for (int idx = threadIdx.x + blockDim.x * blockIdx.x;
         idx < count;
         idx += gridDim.x * blockDim.x)
    {
        temp += input[idx];
    }

    // 将各线程的局部和存入共享内存
    // ​​这256个block是独立在SM上调度的,每个block中的256个线程共享同一个共享内存空间,但不同block的共享内存是完全隔离的​​
    sum_per_block[threadIdx.x] = temp;
    // 确保当前block中所有线程完成写入后再继续
    __syncthreads();

    //**********shared memory summation stage***********
    // 每次迭代将数据规模减半(类似折半求和)
    for (int length = BLOCK_SIZE / 2; length >= 1; length /= 2)
    {
        int double_kill = -1;
        // 仅低索引线程参与计算(每次减半)
        if (threadIdx.x < length)
        {
            // 例:1 2 3 4,这里将1和3相加,2和4相加
            double_kill = sum_per_block[threadIdx.x] + sum_per_block[threadIdx.x + length];
        }
        // 确保所有线程完成对共享内存的​​读取​​(避免部分线程写入前被读取)
        __syncthreads(); 

        if (threadIdx.x < length)
        {
            // 结果回写到共享内存的前半段(覆盖旧值)
            sum_per_block[threadIdx.x] = double_kill;
        }
        // 确保写入操作完成后再进入下一轮归约(防止脏数据)
        __syncthreads();

    } // the per-block partial sum is sum_per_block[0]

    // 跳过超出数组范围的线程块(如网格过大时),避免无效线程块的冗余原子操作
    if (blockDim.x * blockIdx.x < count)
    {
        // the final reduction performed by atomicAdd()
        // 每个block的 0 号线程将块部分和累加到全局输出
        // atomicAdd确保跨线程块的原子操作
        if (threadIdx.x == 0)
            atomicAdd(output, sum_per_block[0]);
    }
}

int _sum_cpu(int *ptr, int count)
{
    int sum = 0;
    for (int i = 0; i < count; i++)
    {
        sum += ptr[i];
    }
    return sum;
}

int main() {
    fprintf(stderr, "filling the buffer with %d random elements...\n", N);
    _init(source, N);

    cudaDeviceSynchronize();

    fprintf(stderr, "Running on GPU...\n");

    double t0 = get_time();
    _sum_gpu<<<BLOCKS, BLOCK_SIZE>>>(source, N, final_result);
    CHECK(cudaGetLastError());      // checking for launch failures
    CHECK(cudaDeviceSynchronize()); // checking for run-time failurs
    double t1 = get_time();

    int A = final_result[0];
    fprintf(stderr, "GPU sum: %u\n", A);

    fprintf(stderr, "Running on CPU...\n");

    double t2 = get_time();
    int B = _sum_cpu(source, N);
    double t3 = get_time();
    fprintf(stderr, "CPU sum: %u\n", B);

    //******The last judgement**********
    if (A == B)
    {
        fprintf(stderr, "Test Passed!\n");
    }
    else
    {
        fprintf(stderr, "Test failed!\n");
        exit(-1);
    }

    //****and some timing details*******
    fprintf(stderr, "GPU time %.3f ms\n", (t1 - t0) * 1000.0);
    fprintf(stderr, "CPU time %.3f ms\n", (t3 - t2) * 1000.0);

    return 0;
}

运行结果:


网站公告

今日签到

点亮在社区的每一天
去签到