数组的并行求和-cuda实现

简介

参考:https://developer.download.nvidia.com/assets/cuda/files/reduction.pdf

NVIDIA 官方有一个PPT是介绍reduce sum,就是对数组进行求和。这个在串行程序里面非常简单的程序,在并行里面实现却有很多的技巧。PPT的最后给出了最终优化的版本,非常值得学习,但是网上几乎没有运行这个程序的demo。这里给出一个简单的demo程序。

完整代码

#include <cuda_runtime_api.h>
#include <cuda.h>
#include <device_functions.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>

#define CUDA_ERROR_CHECK
#define WARP_SIZE 32
#define MAX_THREAD 512
#define CudaCheckError()    __cudaCheckError( __FILE__, __LINE__ )

inline void __cudaCheckError( const char *file, const int line )
{
#ifdef CUDA_ERROR_CHECK
    cudaError err = cudaGetLastError();
    if ( cudaSuccess != err )
    {
        fprintf( stderr, "cudaCheckError() failed at %s:%i : %s\n",
                 file, line, cudaGetErrorString( err ) );
        exit( -1 );
    }

    // More careful checking. However, this will affect performance.
    // Comment away if needed.
    err = cudaDeviceSynchronize();
    if( cudaSuccess != err )
    {
        fprintf( stderr, "cudaCheckError() with sync failed at %s:%i : %s\n",
                 file, line, cudaGetErrorString( err ) );
        exit( -1 );
    }
#endif

    return;
}

__device__ void warpReduce(volatile int *sdata, unsigned int tid, int blockSize)
{
    if (blockSize >= 64) {sdata[tid] += sdata[tid + 32];}
    if (blockSize >= 32) {sdata[tid] += sdata[tid + 16];}
    if (blockSize >= 16) {sdata[tid] += sdata[tid + 8];}
    if (blockSize >= 8) {sdata[tid] += sdata[tid + 4];}
    if (blockSize >= 4) {sdata[tid] += sdata[tid + 2];}
    if (blockSize >= 2) {sdata[tid] += sdata[tid + 1];}
}

__global__ void reduce6(int *g_idata, int *g_odata, unsigned int n, int blockSize) 
{
    extern __shared__ int sdata[];
    
    unsigned int tid = threadIdx.x;
    unsigned int i = blockIdx.x*(blockSize*2) + tid; 
    unsigned int gridSize = blockSize*2*gridDim.x; //suit for the grid_dim.y > 1

    sdata[tid] = 0;//set the shared memory to zero is needed
    while (i < n) 
    { 
        sdata[tid] += g_idata[i];
        if ((i+blockSize) < n)
            sdata[tid] += g_idata[i+blockSize];
        
        i += gridSize; 
    }
     __syncthreads();
    if (blockSize >= 512) 
    { 
        if (tid < 256)
        { 
            sdata[tid] += sdata[tid + 256]; 
        }
        __syncthreads(); 
    } 
    if (blockSize >= 256) 
    { 
        if (tid < 128) 
        { 
            sdata[tid] += sdata[tid + 128]; 
        }
        __syncthreads(); 
    } 
    if (blockSize >= 128)
    { 
        if (tid < 64)
        { 
            sdata[tid] += sdata[tid + 64]; 
        } 
        __syncthreads(); 
    }
    if (tid < 32) warpReduce(sdata, tid, blockSize);
    if (tid == 0) g_odata[blockIdx.x] = sdata[0]; 
}

unsigned int calc_next_32_times(unsigned int x)
{
    unsigned int next_pow_32_times = WARP_SIZE;
    for(int i=2; next_pow_32_times < x; i++)
        next_pow_32_times = WARP_SIZE*i;
    
    return next_pow_32_times;
}

int configure_reduce_sum(dim3 &grid_dim, dim3 &block_dim, unsigned int next_pow_32_times)
{
    if(next_pow_32_times < MAX_THREAD)
    {
        block_dim = dim3(next_pow_32_times, 1, 1);
        grid_dim = dim3(1,1,1);
    }
    else
    {
        int grid_x = ceil((double) next_pow_32_times / MAX_THREAD);
        block_dim = dim3(MAX_THREAD, 1, 1);
        grid_dim = dim3(grid_x,1,1);
    }

    return 0;
}

extern "C" 
long API_cuda_reduce_sum(int *arr, unsigned int total_num)
{
    int *d_arr, *d_out_arr, *h_out_arr;
    unsigned int next_pow_32_times = calc_next_32_times(total_num);

    cudaMalloc(&d_arr, next_pow_32_times * sizeof(int));
    cudaMemset(d_arr, 0, next_pow_32_times * sizeof(int));
    
    dim3 block_dim, grid_dim;
    configure_reduce_sum(grid_dim, block_dim, next_pow_32_times);
    
    int block_num = grid_dim.x * grid_dim.y * grid_dim.z;
   
    cudaMalloc(&d_out_arr, block_num * sizeof(int));
    cudaMemset(d_out_arr, 0, block_num * sizeof(int));
    CudaCheckError();

    cudaMemcpy(d_arr, arr, sizeof(int) * total_num, cudaMemcpyHostToDevice);
    
    reduce6<<<grid_dim, block_dim, block_dim.x*sizeof(int)>>>(d_arr, d_out_arr, total_num, block_dim.x);
    CudaCheckError();
    cudaDeviceSynchronize();

    h_out_arr = new int[block_num];
    memset(h_out_arr, 0, sizeof(int)*block_num);
    cudaMemcpy(h_out_arr, d_out_arr, sizeof(int) * block_num, cudaMemcpyDeviceToHost);

    long ret=0;
    for(int i=0; i<block_num; i++)
    {
        ret += h_out_arr[i];
    }

    cudaFree(d_arr);
    cudaFree(d_out_arr);
    delete []h_out_arr;

    return ret;

}

int main(int argc, char **argv)
{
    int arr[] = {1,2,3,4,4,5,5,6,7,3,2,3,1,1,1,1,1,4,1,2,3,4,4,5,5,6,7,3,2,3,1,1,1,1,1,4,1,2,3,4,4,5,5,6,7,3,2,3,1,1,1,1,1,4,1,2,3,4,4,5,5,6,7,3,2,3,1,1,1,1,1,4,1,1,1,4,1,1,1,4}; //80
    int arr_len = sizeof(arr)/sizeof(arr[0]);
    int times = 5000000;
    int *big_arr = new int[times*arr_len];
    long long sum = 0;
    clock_t start, finish;

    for(int i=0; i<times; i++)
    {
        memcpy((void*)(big_arr + (i*arr_len)), (void*)arr, sizeof(int)*arr_len);
    }

    start = clock();
    for(int i=0; i<arr_len*times; i++)
    {  
        sum += big_arr[i];
    }
    finish = clock();
    printf("arr len %ld\n", arr_len*times);
    printf("serial sum of arr: %ld, cost time: %.4f s\n", sum,  (double) (finish - start) / CLOCKS_PER_SEC);

    start = clock();
    int cuda_sum = API_cuda_reduce_sum(big_arr, arr_len*times);
    finish = clock();

    printf("cuda sum of arr: %ld, cost time: %.4f s\n", cuda_sum, (double) (finish - start) / CLOCKS_PER_SEC);
    return 0;

}

 

要实现完整的求和需要递归的调用kernel函数,这里只实现了第一层的reduce,也就是每个block得到一个sum,最后对每个block的sum串行的求和得到最终的sum。

编译运行

nvcc cuda_reduce_sum.c -o cuda_reduce

./cuda_reduce

  

测试

程序输出:

arr len 400000000

serial sum of arr: 1150000000, cost time: 0.8800

cuda sum of arr: 1150000000, cost time: 0.5500

测试过程中发现,如果数组较小,串行的求和比cuda的方式更快。因为,使用cuda求和,需要进行内存搬运和kernel函数的launch等费时操作。所以在数据量较少时,使用串行求和的效率更高。

 

 

 
posted @ 2019-12-04 19:51  walter_xh  阅读(3372)  评论(0编辑  收藏  举报