2013-11-18 5 views
1

Я работаю над созданием игровой программы жизненного цикла на основе графического процессора. Если вы не знакомы с этим, вот Wikipedia Page. Я создал одну версию, которая работает, сохраняя массив значений, где 0 - мертвая ячейка, а 1 - живая. Затем ядро ​​просто записывает в массив данных буфера изображений, чтобы нарисовать изображение на основе данных ячейки, а затем проверяет соседние ячейки каждой ячейки для обновления массива ячеек для последующего выполнения рендеринга.OpenCL Общая память среди задач

Однако, более быстрый метод вместо этого представляет значение ячейки как отрицательное число, если мертвое и положительное число, если оно живое. Число этой ячейки представляет собой количество соседей, которые она имеет плюс один (делая нуль невозможным, поскольку мы не можем отличить 0 от -0). Однако это означает, что при нерестах или убийстве ячейки мы должны соответствующим образом обновить значения восьми соседей. Таким образом, в отличие от рабочей процедуры, которая должна считываться только из соседних слотов памяти, эта процедура должна записываться в эти слоты. Это несовместимо, и полученный массив недействителен. Например, ячейки содержат числа, такие как 14, которые указывают 13 соседей, невозможное значение. Код верный, поскольку я написал ту же процедуру на процессоре, и он работает так, как ожидалось. После тестирования я считаю, что когда задачи пытаются записать в память одновременно, происходит задержка, которая приводит к ошибке записи. Например, возможно, существует задержка между чтением данных массива и настройкой, в течение которой данные изменяются, а неправильная процедура другой задачи. Я пробовал использовать семафоры и барьеры, но только что изучил OpenCL и параллельную обработку и не совсем понял их полностью. Ядро выглядит следующим образом.

int wrap(int val, int limit){ 
    int response = val; 
    if(response<0){response+=limit;} 
    if(response>=limit){response-=limit;} 
    return response; 
} 

__kernel void optimizedModel(
     __global uint *output, 
     int sizeX, int sizeY, 
     __global uint *colorMap, 
     __global uint *newCellMap, 
     __global uint *historyBuffer 
) 
{ 
    // the x and y coordinates that currently being computed 
    unsigned int x = get_global_id(0); 
    unsigned int y = get_global_id(1); 

    int cellValue = historyBuffer[sizeX*y+x]; 
    int neighborCount = abs(cellValue)-1; 
    output[y*sizeX+x] = colorMap[cellValue > 0 ? 1 : 0]; 

    if(cellValue > 0){// if alive 
     if(neighborCount < 2 || neighborCount > 3){ 
      // kill 

      for(int i=-1; i<2; i++){ 
       for(int j=-1; j<2; j++){ 
        if(i!=0 || j!=0){ 
         int wxc = wrap(x+i, sizeX); 
         int wyc = wrap(y+j, sizeY); 
         newCellMap[sizeX*wyc+wxc] -= newCellMap[sizeX*wyc+wxc] > 0 ? 1 : -1; 
        } 
       } 
      } 
      newCellMap[sizeX*y+x] *= -1; 

      // end kill 
     } 
    }else{ 
     if(neighborCount==3){ 
      // spawn 

      for(int i=-1; i<2; i++){ 
       for(int j=-1; j<2; j++){ 
        if(i!=0 || j!=0){ 
         int wxc = wrap(x+i, sizeX); 
         int wyc = wrap(y+j, sizeY); 
         newCellMap[sizeX*wyc+wxc] += newCellMap[sizeX*wyc+wxc] > 0 ? 1 : -1; 
        } 
       } 
      } 
      newCellMap[sizeX*y+x] *= -1; 

      // end spawn 
     } 
    } 
} 
  1. Массив выход это данные буфера изображения, используемые для визуализации вычислений в ядра.
  2. sizeX и sizeY константы - ширина и высота буфера изображения соответственно.
  3. массив colorMap содержит значения целочисленного значения rgb для черно-белого изображения соответственно, которые используются для правильного изменения значений буфера изображения для рендеринга цветов.
  4. newCellMap массив - это обновленная карта ячейки, которая рассчитывается после определения рендеринга.
  5. historyBuffer - это старое состояние ячеек в начале вызова ядра. Каждый раз, когда ядро ​​выполняется, этот массив обновляется до массива newCellMap.

Дополнительно обертывание Функция делает пространство тороидальным. Как я могу исправить этот код так, чтобы он работал так, как ожидалось. И почему глобальное обновление памяти с каждым изменением задачи? Разве это не должно быть разделяемой памятью?

+0

Ответ довольно прост. Считывание и запись в одно и то же место памяти из разных потоков в одном вызове ядра не определено. Единственный способ заставить их работать - это барьеры, и даже они работают только в одной рабочей группе. – sharpneli

+0

Значит, это не произойдет в ячейках, граничащих с рабочей группой? Точно так же, как будет барьер в действии? Потому что это не все задачи. –

+0

@HunterLarco Да, он будет терпеть неудачу в ячейках, граничащих с рабочей группой, но только если вы установите надлежащие барьеры. Как вы этого не сделаете, он не работает в любой ячейке. У вас не может быть барьера внутри if, поскольку барьер ** HAS ** встречается всеми рабочими предметами. – DarkZeros

ответ

1

Как отметил в своем ответе sharpneli, вы читаете и записываете одинаковые зоны памяти из разных потоков, что дает неопределенное поведение.

Решение: Вы должны разделить ваши newCellMap в 2 массивы, один для предыдущего выполнения и тот, где новое значение будет сохранено. Затем вам необходимо изменить аргументы ядра со стороны хоста в каждом вызове, так что oldvalues следующей итерации - это newvalues предыдущей итерации. Из-за того, как вы структурируете свой алгоритм, вам также необходимо выполнить копию буфера oldvalues до newvalues, прежде чем запускать его.

__kernel void optimizedModel(
     __global uint *output, 
     int sizeX, int sizeY, 
     __global uint *colorMap, 
     __global uint *oldCellMap, 
     __global uint *newCellMap, 
     __global uint *historyBuffer 
) 
{ 
    // the x and y coordinates that currently being computed 
    unsigned int x = get_global_id(0); 
    unsigned int y = get_global_id(1); 

    int cellValue = historyBuffer[sizeX*y+x]; 
    int neighborCount = abs(cellValue)-1; 
    output[y*sizeX+x] = colorMap[cellValue > 0 ? 1 : 0]; 

    if(cellValue > 0){// if alive 
     if(neighborCount < 2 || neighborCount > 3){ 
      // kill 

      for(int i=-1; i<2; i++){ 
       for(int j=-1; j<2; j++){ 
        if(i!=0 || j!=0){ 
         int wxc = wrap(x+i, sizeX); 
         int wyc = wrap(y+j, sizeY); 
         newCellMap[sizeX*wyc+wxc] -= oldCellMap[sizeX*wyc+wxc] > 0 ? 1 : -1; 
        } 
       } 
      } 
      newCellMap[sizeX*y+x] *= -1; 

      // end kill 
     } 
    }else{ 
     if(neighborCount==3){ 
      // spawn 

      for(int i=-1; i<2; i++){ 
       for(int j=-1; j<2; j++){ 
        if(i!=0 || j!=0){ 
         int wxc = wrap(x+i, sizeX); 
         int wyc = wrap(y+j, sizeY); 
         newCellMap[sizeX*wyc+wxc] += oldCellMap[sizeX*wyc+wxc] > 0 ? 1 : -1; 
        } 
       } 
      } 
      newCellMap[sizeX*y+x] *= -1; 

      // end spawn 
     } 
    } 
} 

Что касается вашего вопроса об общей памяти, то есть простой ответ. OpenCL не имеет общей памяти через HOST-DEVICE.

Когда вы создаете буфер памяти для устройства, вам сначала нужно инициализировать эту зону памяти с помощью clEnqueueWriteBuffer() и прочитать ее с помощью clEnqueueWriteBuffer(), чтобы получить результаты. Даже если у вас есть указатель на зону памяти, ваш указатель является указателем на копию стороны хоста этой зоны. Скорее всего, не будет последней версии вычисляемого вывода устройства.

PD: Я создал давнюю игру «Live» на OpenCL, я обнаружил, что более простой и быстрый способ сделать это - просто создать большой 2D-массив бит (битовая адресация). А затем напишите фрагмент кода без каких-либо ветвей, которые просто анализируют neibours и получают обновленное значение для этой ячейки. Поскольку используется бит-адресация, количество чтения/записи памяти по каждому потоку значительно ниже, чем адресация символов/ints/other. Я достиг 33Mcells/sec в очень старом OpenCL HW (nVIDIA 9100M G). Просто чтобы вы знали, что ваш подход if/else, вероятно, не самый эффективный.

1

Подобно тому, как ссылки, я вас здесь моя реализация игры жизни (OpenCL ядра):

//Each work-item processess one 4x2 block of cells, but needs to access to the (3x3)x(4x2) block of cells surrounding it 
// . . . . . . 
// . * * * * . 
// . * * * * . 
// . . . . . . 

__kernel void life (__global unsigned char * input, __global unsigned char * output){ 

    int x_length = get_global_size(0); 
    int x_id = get_global_id(0); 
    int y_length = get_global_size(1); 
    int y_id = get_global_id(1); 
    //int lx_length = get_local_size(0); 
    //int ly_length = get_local_size(1); 

    int x_n = (x_length+x_id-1)%x_length; //Negative X 
    int x_p = (x_length+x_id+1)%x_length; //Positive X 
    int y_n = (y_length+y_id-1)%y_length; //Negative Y 
    int y_p = (y_length+y_id+1)%y_length; //Positive X 

    //Get the data of the surrounding blocks (TODO: Make this shared across the local group) 
    unsigned char block[3][3]; 
    block[0][0] = input[x_n + y_n*x_length]; 
    block[1][0] = input[x_id + y_n*x_length]; 
    block[2][0] = input[x_p + y_n*x_length]; 
    block[0][1] = input[x_n + y_id*x_length]; 
    block[1][1] = input[x_id + y_id*x_length]; 
    block[2][1] = input[x_p + y_id*x_length]; 
    block[0][2] = input[x_n + y_p*x_length]; 
    block[1][2] = input[x_id + y_p*x_length]; 
    block[2][2] = input[x_p + y_p*x_length]; 

    //Expand the block to points (bool array) 
    bool point[6][4]; 
    point[0][0] = (bool)(block[0][0] & 1); 
    point[1][0] = (bool)(block[1][0] & 8); 
    point[2][0] = (bool)(block[1][0] & 4); 
    point[3][0] = (bool)(block[1][0] & 2); 
    point[4][0] = (bool)(block[1][0] & 1); 
    point[5][0] = (bool)(block[2][0] & 8); 
    point[0][1] = (bool)(block[0][1] & 16); 
    point[1][1] = (bool)(block[1][1] & 128); 
    point[2][1] = (bool)(block[1][1] & 64); 
    point[3][1] = (bool)(block[1][1] & 32); 
    point[4][1] = (bool)(block[1][1] & 16); 
    point[5][1] = (bool)(block[2][1] & 128); 
    point[0][2] = (bool)(block[0][1] & 1); 
    point[1][2] = (bool)(block[1][1] & 8); 
    point[2][2] = (bool)(block[1][1] & 4); 
    point[3][2] = (bool)(block[1][1] & 2); 
    point[4][2] = (bool)(block[1][1] & 1); 
    point[5][2] = (bool)(block[2][1] & 8); 
    point[0][3] = (bool)(block[0][2] & 16); 
    point[1][3] = (bool)(block[1][2] & 128); 
    point[2][3] = (bool)(block[1][2] & 64); 
    point[3][3] = (bool)(block[1][2] & 32); 
    point[4][3] = (bool)(block[1][2] & 16); 
    point[5][3] = (bool)(block[2][2] & 128); 

    //Process one point of the game of life! 
    unsigned char out = (unsigned char)0; 
    for(int j=0; j<2; j++){ 
     for(int i=0; i<4; i++){ 
      char num = point[i][j] + point[i+1][j] + point[i+2][j] + point[i][j+1] + point[i+2][j+1] + point[i][j+2] + point[i+1][j+2] + point[i+2][j+2]; 
      if(num == 3 || num == 2 && point[i+1][j+1]){ 
       out |= (128>>(i+4*j)); 
      } 
     } 
    } 
    output[x_id + y_id*x_length] = out; //Assign to the output the new cells value 
}; 

Здесь вы не сохраняете никаких промежуточных состояний, только статус ячейки в конце (живая/смерть). Он не имеет филиалов, поэтому он довольно быстро в этом процессе.

Смежные вопросы