【问题标题】:CUDA Dot ProductCUDA 点积
【发布时间】:2012-02-26 00:48:03
【问题描述】:

我正在尝试为双精度数组实现经典的点积内核,并对各个块的最终总和进行原子计算。我使用 atomicAdd 进行双精度,如编程指南第 116 页所述。可能我做错了什么。每个块中线程的部分总和计算正确,但原子操作似乎无法正常工作因为每次我用相同的数据运行我的内核时,我都会收到不同的结果。如果有人能发现错误或提供替代解决方案,我将不胜感激! 这是我的内核:

__global__ void cuda_dot_kernel(int *n,double *a, double *b, double *dot_res)
{
    __shared__ double cache[threadsPerBlock]; //thread shared memory
    int global_tid=threadIdx.x + blockIdx.x * blockDim.x;
    int i=0,cacheIndex=0;
    double temp = 0;
    cacheIndex = threadIdx.x;
    while (global_tid < (*n)) {
        temp += a[global_tid] * b[global_tid];
        global_tid += blockDim.x * gridDim.x;
    }
    cache[cacheIndex] = temp;
    __syncthreads();
    for (i=blockDim.x/2; i>0; i>>=1) {
        if (threadIdx.x < i) {
            cache[threadIdx.x] += cache[threadIdx.x + i];
        }
        __syncthreads();
    }
    __syncthreads();
    if (cacheIndex==0) {
        *dot_res=cuda_atomicAdd(dot_res,cache[0]);
    }
}

这是我的设备函数 atomicAdd:

__device__ double cuda_atomicAdd(double *address, double val)
{
    double assumed,old=*address;
    do {
        assumed=old;
        old= __longlong_as_double(atomicCAS((unsigned long long int*)address,
                    __double_as_longlong(assumed),
                    __double_as_longlong(val+assumed)));
    }while (assumed!=old);

    return old;
}

【问题讨论】:

  • 共享内存原子非常慢。这不是实现点积的好方法。正如 Jared 指出的,你最好使用 Thrust。如果您坚持编写自己的代码,并且确实想在单个内核中完成,请参阅 CUDA SDK 代码示例中的 threadFenceReduction 示例。它应该更有效(它不是点积,只是求和,但添加初始元素乘法应该是微不足道的。)
  • @harrism:这段代码中哪里有共享内存原子?这只是一个标准的共享内存缩减,使用全局内存原子操作来完成块级部分缩减值的总和。
  • 对不起,我把原子论点换了!无论如何,如果您使用 threadfence,则不需要原子来在单个内核中实现归约。

标签: cuda dot-product


【解决方案1】:

使用 ad hoc CUDA 代码正确地减少可能会很棘手,因此这里有一个使用推力算法的替代解决方案,它包含在 CUDA 工具包中:

#include <thrust/inner_product.h>
#include <thrust/device_ptr.h>

double do_dot_product(int n, double *a, double *b)
{
  // wrap raw pointers to device memory with device_ptr
  thrust::device_ptr<double> d_a(a), d_b(b);

  // inner_product implements a mathematical dot product
  return thrust::inner_product(d_a, d_a + n, d_b, 0.0);
}

【讨论】:

    【解决方案2】:

    您错误地使用了cuda_atomicAdd 函数。这部分内核:

    if (cacheIndex==0) {
        *dot_res=cuda_atomicAdd(dot_res,cache[0]);
    }
    

    是罪魁祸首。在这里,您以原子方式添加到dot_res。然后非原子地dot_res 设置为它返回的结果。此函数的返回结果是被原子更新的位置的上一个值,它仅提供给调用者的“信息”或本地使用。您没有将它分配给您以原子方式更新的内容,这完全违背了首先使用原子内存访问的目的。改为这样做:

    if (cacheIndex==0) {
        double result=cuda_atomicAdd(dot_res,cache[0]);
    }
    

    【讨论】:

    • 感谢您的回复。由于全局变量 *dot_res 已初始化为 0,因此我将拥有 gridDim.x 块,其中包含与共享变量缓存相同的值的局部变量“result” [0] 对(result=cache[0]+*dot_res=cache[0])?如果我理解正确,这种方式不会有最终的减少。有没有办法在设备上完成减少?我尝试使用示例中的 cuda 中的互斥锁示例,但它似乎产生了死锁。
    • 我不确定我是否理解您的要求。如果你只是做出我展示的改变,我相信它应该像你想象的那样工作,并且应该完成减少。 atomicCAS 循环应该只是敲击,直到每个调用线程的贡献已在全局总数中注册。因为您可能只运行 10 到 100 个块之间的东西,所以 dot_res 不应该有太多争用,它应该可以正常工作。
    • 我问的是变量result。这个变量有局部作用域对吗?只有cacheIndex=0的线程才能查看这个变量的专有副本并修改它?那我怎么去全局,跨所有块只产生 1 个包含所有块的部分和的结果变量?
    • result 无关紧要。您不需要它来使代码正常工作。如果您愿意,您可以重写原子添加以不返回它。它的值是dot_res上一个 值,而不是新值。原子添加函数在内部更新dot_res 本身,即存储点积的位置。
    • Atomic 操作中的舍入误差是否会显着影响最终结果?因为我使用 atomicAdd 执行最终求和时得到的结果与执行串行操作时得到的结果略有不同CPU 最终总和。此外,通过原子操作获取的结果没有任何可重现性。
    【解决方案3】:

    没有检查你的代码的深度,但这里有一些建议。
    如果您只将 GPU 用于此类通用任务,我只会建议您使用 Thrust,因为如果出现复杂问题,人们不知道如何在 GPU 上高效地进行并行编程。

    1. 启动一个新的并行归约内核来总结点积。
      由于数据已经在设备上,您不会看到启动新内核的性能下降。

    2. 您的内核似乎无法跨越最新 GPU 上的最大可能块数。如果它可以并且您的内核将能够计算数百万个值的点积,那么由于序列化的原子操作,性能会急剧下降。

    3. 初学者错误:您的输入数据和共享内存访问是否范围检查?或者您确定输入数据始终是您的块大小的倍数?否则你会读垃圾。我的大部分错误结果都是由于这个错误造成的。

    4. 优化您的并行缩减。 My ThesisOptimisations Mark Harris

    未经测试,我只是在记事本中写了下来:

    /*
     * @param inCount_s unsigned long long int Length of both input arrays
     * @param inValues1_g double* First value array
     * @param inValues2_g double* Second value array
     * @param outDots_g double* Output dots of each block, length equals the number of blocks
     */
    __global__ void dotProduct(const unsigned long long int inCount_s,
        const double* inValuesA_g,
        const double* inValuesB_g,
        double* outDots_g)
    {
        //get unique block index in a possible 3D Grid
        const unsigned long long int blockId = blockIdx.x //1D
                + blockIdx.y * gridDim.x //2D
                + gridDim.x * gridDim.y * blockIdx.z; //3D
    
    
        //block dimension uses only x-coordinate
        const unsigned long long int tId = blockId * blockDim.x + threadIdx.x;
    
        /*
         * shared value pair products array, where BLOCK_SIZE power of 2
         *
         * To improve performance increase its size by multiple of BLOCK_SIZE, so that each threads loads more then 1 element!
         * (outDots_g length decreases by same factor, and you need to range check and initialize memory)
         * -> see harris gpu optimisations / parallel reduction slides for more informations.
         */
        __shared__ double dots_s[BLOCK_SIZE];
    
    
        /*
         * initialize shared memory array and calculate dot product of two values, 
         * shared memory always needs to be initialized, its never 0 by default, else garbage is read later!
         */
        if(tId < inCount_s)
            dots_s[threadIdx.x] = inValuesA_g[tId] * inValuesB_g[tId];
        else
            dots_s[threadIdx.x] = 0;
        __syncthreads();
    
        //do parallel reduction on shared memory array to sum up values
        reductionAdd(dots_s, dots_s[0]) //see my thesis link
    
        //output value
        if(threadIdx.x == 0)
            outDots_g[0] = dots_s[0];
    
        //start new parallel reduction kernel to sum up outDots_g!
    }
    

    编辑:删除不必要的点。

    【讨论】:

    • 1. “内核应该以足够的块来运行,以填充 GPU 中的每个 SM。”谁说它不应该只用足够的块来运行?我说内核本身应该在最大数量的块上是可扩展的! 2. 对于这个简单的内核,不需要任何跨步。此处适用最简单的合并读取模式:developer.download.nvidia.com/compute/cuda/2_0/docs/… 图 5-1
    • 2. “第 5 点也是错误的。”基本的c知识。不要在指针长度之外阅读。您将只读取该内存地址上的任何内容。对于共享内存:stackoverflow.com/questions/6478098/…
    • 第 3 点仍然不适用。也许你不明白这段代码是做什么的,但它在累加循环中隐式的全局内存范围检查。
    • @talonmies "初学者错误:你的输入数据和共享内存访问范围检查了吗?"这就是我问的原因;)如果没有为我们注释代码,为什么有人要花时间搜索错误,所以我只提供基本建议。他最初的问题包括“......或提供替代解决方案!”这就是我所做的,并且表现更好。
    猜你喜欢
    • 2016-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-01
    • 2018-11-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多