使用OpenCL进行累积数组求和

时间:2010-09-22 14:55:33

标签: cuda concurrency opencl

我正在使用OpenCL计算n维点之间的欧氏距离。我得到两个n维点列表,我应该返回一个数组,其中只包含第一个表中每个点到第二个表中每个点的距离。

我的方法是做常规的doble循环(对于Table1中的每个点{对于Table2 {...}}中的每个点,然后对并列中的每对点进行计算。

欧几里德距离分为3部分: 1.取得各点之间的差异 2.平方差异(仍为每个维度) 3.总结2中获得的所有值。 4.取3中获得的值的平方根。(此示例中省略了此步骤。)

在尝试累积所有差异的总和之前,一切都像魅力一样(即,执行上述程序的第3步,下面代码的第49行)。

作为测试数据,我使用DescriptorLists,每个点数为2分: DescriptorList1:001,002,003,...,127,128; (P1)                  129130131,...,255,256; (P2)

DescriptorList2:000,001,002,...,126,127; (P1)                  128129130,...,254,255; (P2)

因此,结果向量应具有值:128,2064512,2130048,128 现在我得到的随机数随每次运行而变化。

我感谢任何帮助或引导我做错了什么。希望一切都清楚我正在工作的场景。

#define BLOCK_SIZE 128

typedef struct
{
    //How large each point is
    int length;
    //How many points in every list
    int num_elements;
    //Pointer to the elements of the descriptor (stored as a raw array)
    __global float *elements;
} DescriptorList;

__kernel void CompareDescriptors_deb(__global float *C, DescriptorList A, DescriptorList B, int elements, __local float As[BLOCK_SIZE])
{

    int gpidA = get_global_id(0);

    int featA = get_local_id(0);

    //temporary array  to store the difference between each dimension of 2 points
    float dif_acum[BLOCK_SIZE];

    //counter to track the iterations of the inner loop
    int loop = 0;

    //loop over all descriptors in A
    for (int i = 0; i < A.num_elements/BLOCK_SIZE; i++){

        //take the i-th descriptor. Returns a DescriptorList with just the i-th
        //descriptor in DescriptorList A
        DescriptorList tmpA = GetDescriptor(A, i);

        //copy the current descriptor to local memory.
        //returns one element of the only descriptor in DescriptorList tmpA
        //and index featA
        As[featA] = GetElement(tmpA, 0, featA);
        //wait for all the threads to finish copying before continuing
        barrier(CLK_LOCAL_MEM_FENCE);

        //loop over all the descriptors in B
        for (int k = 0; k < B.num_elements/BLOCK_SIZE; k++){
            //take the difference of both current points
            dif_acum[featA] = As[featA]-B.elements[k*BLOCK_SIZE + featA];
            //wait again
            barrier(CLK_LOCAL_MEM_FENCE);
            //square value of the difference in dif_acum and store in C
            //which is where the results should be stored at the end.
            C[loop] = 0;
            C[loop] += dif_acum[featA]*dif_acum[featA];
            loop += 1;
            barrier(CLK_LOCAL_MEM_FENCE);
        }
    }
}

2 个答案:

答案 0 :(得分:7)

您的问题在于以下几行代码:

C[loop] = 0;
C[loop] += dif_acum[featA]*dif_acum[featA];

工作组中的所有线程(实际上是所有线程,但稍后再来),它们试图同时修改此数组位置而不进行任何同步。有几个因素会导致这个问题:

  1. 工作组不能保证完全并行工作,这意味着对于某些线程,在其他线程已经执行下一行之后可以调用C [loop] = 0
  2. 并行执行的所有内容都从C [loop]读取相同的值,用它们的增量修改它并尝试写回同一个地址。我不完全确定该回写的结果是什么(我认为其中一个线程成功写回,而其他线程失败,但我不完全确定),但它的错误无论如何。
  3. 现在让我们解决这个问题: 虽然我们可能能够使用原子来使用全局内存,但它不会很快,所以让我们在本地内存中累积:

    local float* accum;
    ...
    accum[featA] = dif_acum[featA]*dif_acum[featA];
    barrier(CLK_LOCAL_MEM_FENCE);
    for(unsigned int i = 1; i < BLOCKSIZE; i *= 2)
    {
        if ((featA % (2*i)) == 0)
            accum[featA] += accum[featA + i];
        barrier(CLK_LOCAL_MEM_FENCE);
    }
    if(featA == 0)
        C[loop] = accum[0];
    

    当然你可以为此重用其他本地缓冲区,但我认为这一点很明确(顺便说一句:你确定dif_acum将在本地内存中创建,因为我想我读到某个地方不会放入本地内存,这会使A预加载到本地内存中毫无意义。)

    关于此代码的其他一些观点:

    1. 您的代码似乎只适用于工作组(您没有使用groupid或全局ID来查看要处理的项目),为了获得最佳性能,您可能希望使用更多。
    2. 可能是个人优先,但对我来说,使用get_local_size(0)作为工作组大小似乎比使用定义更好(因为您可能在主机代码中更改它而没有意识到您应该将opencl代码更改为)
    3. 代码中的障碍都是不必要的,因为没有线程访问本地内存中由另一个线程写入的元素。因此,您不需要使用本地内存。
    4. 考虑到你可以做的最后一颗子弹:

      float As = GetElement(tmpA, 0, featA);
      ...
      float dif_acum = As-B.elements[k*BLOCK_SIZE + featA];
      

      这会产生代码(不考虑前两个子弹):

      __kernel void CompareDescriptors_deb(__global float *C, DescriptorList A, DescriptorList B, int elements, __local float accum[BLOCK_SIZE])
      {
         int gpidA = get_global_id(0);
         int featA = get_local_id(0);
         int loop = 0;
         for (int i = 0; i < A.num_elements/BLOCK_SIZE; i++){
             DescriptorList tmpA = GetDescriptor(A, i);
             float As = GetElement(tmpA, 0, featA);
             for (int k = 0; k < B.num_elements/BLOCK_SIZE; k++){
                 float dif_acum = As-B.elements[k*BLOCK_SIZE + featA];
      
                 accum[featA] = dif_acum[featA]*dif_acum[featA];
                 barrier(CLK_LOCAL_MEM_FENCE);
                 for(unsigned int i = 1; i < BLOCKSIZE; i *= 2)
                 {
                    if ((featA % (2*i)) == 0)
                       accum[featA] += accum[featA + i];
                    barrier(CLK_LOCAL_MEM_FENCE);
                 }
                 if(featA == 0)
                    C[loop] = accum[0];
                 barrier(CLK_LOCAL_MEM_FENCE);
      
                 loop += 1;
              }
          }
      }
      

答案 1 :(得分:3)

感谢Grizzly,我现在有了一个有效的内核。我需要根据Grizzly的答案修改一些事情:

我在例程的开头添加了一个IF语句,以丢弃所有不会引用我正在使用的数组中任何有效位置的线程。

if(featA > BLOCK_SIZE){return;}

当将第一个描述符复制到本地(共享)内存(例如Bs)时,必须指定索引,因为函数GetElement每次调用只返回一个元素(我跳过了我的问题)。

Bs[featA] = GetElement(tmpA, 0, featA);

然后,SCAN循环需要稍微调整,因为缓冲区在每次迭代后被覆盖,并且无法控制哪个线程首先访问数据。这就是为什么我'循环'使用dif_acum缓冲区来存储部分结果,这样就可以防止整个循环中的不一致。

dif_acum[featA] = accum[featA];

SCAN循环中还有一些边界控制可靠地确定要加在一起的术语。

if (featA >= j && next_addend >= 0 && next_addend < BLOCK_SIZE){

最后,我认为在最后一个IF语句中包含循环变量增量是有意义的,这样只有一个线程可以修改它。

if(featA == 0){
    C[loop] = accum[BLOCK_SIZE-1];
    loop += 1;
}

就是这样。我仍然想知道如何使用group_size来消除BLOCK_SIZE定义,以及我是否可以采用更好的策略来使用线程。

所以代码看起来最终是这样的:

__kernel void CompareDescriptors(__global float *C, DescriptorList A, DescriptorList B, int elements, __local float accum[BLOCK_SIZE], __local float Bs[BLOCK_SIZE])
{

    int gpidA = get_global_id(0);
    int featA = get_local_id(0);

    //global counter to store final differences
    int loop = 0;

    //auxiliary buffer to store temporary data
    local float dif_acum[BLOCK_SIZE];

    //discard the threads that are not going to be used.
    if(featA > BLOCK_SIZE){
        return;
    }

    //loop over all descriptors in A
    for (int i = 0; i < A.num_elements/BLOCK_SIZE; i++){

        //take the gpidA-th descriptor
        DescriptorList tmpA = GetDescriptor(A, i);

        //copy the current descriptor to local memory
        Bs[featA] = GetElement(tmpA, 0, featA);

        //loop over all the descriptors in B
        for (int k = 0; k < B.num_elements/BLOCK_SIZE; k++){
            //take the difference of both current descriptors
            dif_acum[featA] = Bs[featA]-B.elements[k*BLOCK_SIZE + featA];

            //square the values in dif_acum
            accum[featA] = dif_acum[featA]*dif_acum[featA];
            barrier(CLK_LOCAL_MEM_FENCE);

            //copy the values of accum to keep consistency once the scan procedure starts. Mostly important for the first element. Two buffers are necesarry because the scan procedure would override values that are then further read if one buffer is being used instead.
            dif_acum[featA] = accum[featA];

            //Compute the accumulated sum (a.k.a. scan)
            for(int j = 1; j < BLOCK_SIZE; j *= 2){
                int next_addend = featA-(j/2);
                if (featA >= j && next_addend >= 0 && next_addend < BLOCK_SIZE){
                    dif_acum[featA] = accum[featA] + accum[next_addend];
                }
                barrier(CLK_LOCAL_MEM_FENCE);

                //copy As to accum
                accum[featA] = GetElementArray(dif_acum, BLOCK_SIZE, featA); 
                barrier(CLK_LOCAL_MEM_FENCE);
            }

            //tell one of the threads to write the result of the scan in the array containing the results.
            if(featA == 0){
                C[loop] = accum[BLOCK_SIZE-1];
                loop += 1;
            }
            barrier(CLK_LOCAL_MEM_FENCE);

        }
    }
}