Суммирование совокупного массива с использованием OpenCL - PullRequest
5 голосов
/ 22 сентября 2010

Я вычисляю евклидово расстояние между n-мерными точками, используя OpenCL. Я получаю два списка n-мерных точек и должен вернуть массив, который содержит только расстояния от каждой точки в первой таблице до каждой точки во второй таблице.

Мой подход заключается в том, чтобы сделать обычный цикл дублирования (для каждой точки в Таблице 1 {для каждой точки в Таблице2 {...}}, а затем выполнить расчет для каждой пары точек в паралеле.

Евклидово расстояние затем делится на 3 части: 1. взять разницу между каждым измерением в точках 2. возвести в квадрат эту разницу (по-прежнему для каждого измерения) 3. Суммируйте все значения, полученные в 2. 4. Возьмите квадратный корень из значения, полученного в 3. (этот шаг был пропущен в этом примере.)

Все работает как брелок, пока я не попытаюсь накопить сумму всех различий (а именно, выполняя шаг 3. описанной выше процедуры, строка 49 кода ниже).

В качестве тестовых данных я использую DescriptorLists с двумя точками каждый: DescriptorList1: 001,002,003, ..., 127,128; (Р1) 129130131, ..., 255256; (P2)

DescriptorList2: 000,001,002, ..., 126,127; (Р1) 128129130, ..., 254255; (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 ]

7 голосов
/ 23 сентября 2010

Ваша проблема заключается в следующих строках кода:

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

Все потоки в вашей рабочей группе (ну, фактически, все ваши потоки, но давайте вернемся к этому позже), пытаются изменить эту позицию массива одновременно без какой-либо синхронизации. Несколько факторов делают это действительно проблематичным:

  1. Рабочая группа не может работать полностью параллельно, что означает, что для некоторых потоков C [loop] = 0 может быть вызвано после того, как другие потоки уже выполнили следующую строку
  2. Те, которые выполняются параллельно, все читают одно и то же значение из C [loop], изменяют его с приращением и пытаются записать обратно по тому же адресу. Я не совсем уверен, каков результат этой обратной записи (я думаю, что один из потоков преуспел в обратной записи, в то время как другие потерпели неудачу, но я не совсем уверен), но в любом случае это неправильно.

Теперь давайте исправим это: Хотя нам, возможно, удастся заставить это работать с глобальной атомной памятью, используя атомику, это не будет быстрым, поэтому давайте накапливаться в локальной памяти:

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, ни global id, чтобы увидеть, над какими элементами работать), для оптимальной производительности вам может потребоваться больше, чем это.
  2. Возможно, это личное предпочтение, но мне кажется, лучше использовать get_local_size(0) для размера рабочей группы, чем использовать Define (поскольку вы можете изменить его в коде хоста, не понимая, что вы должны были изменить код opencl на)
  3. Все барьеры в вашем коде не нужны, поскольку ни один поток не обращается к элементу в локальной памяти, который записан другим потоком. Поэтому вам не нужно использовать локальную память для этого.

Учитывая последнюю пулю, которую вы могли бы просто сделать:

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;
        }
    }
}
3 голосов
/ 24 сентября 2010

Благодаря Grizzly у меня теперь работает ядро.Некоторые вещи, которые мне нужно было изменить, основаны на ответе Гризли:

Я добавил оператор IF в начале процедуры, чтобы отбросить все потоки, которые не будут ссылаться на какую-либо допустимую позицию в массивах, которые я использую.

if(featA > BLOCK_SIZE){return;}

При копировании первого дескриптора в локальную (разделяемую) память (от ig до B) необходимо указывать индекс, поскольку функция 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);

        }
    }
}
...