Я работал над созданием программы жизни Конвея на основе GPU. Если вы не знакомы с этим, вот Страница Википедии. Я создал одну версию, которая работает, сохраняя массив значений, где 0 представляет мертвую ячейку, а 1 — живую. Затем ядро просто записывает данные в массив данных буфера изображения, чтобы нарисовать изображение на основе данных ячейки, а затем проверяет соседей каждой ячейки, чтобы обновить массив ячеек для следующего выполнения рендеринга.
Однако более быстрый метод вместо этого представляет значение ячейки как отрицательное число, если оно мертвое, и положительное число, если оно живое. Номер этой ячейки представляет количество соседей, которые у нее есть, плюс один (делая ноль невозможным значением, поскольку мы не можем отличить 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
}
}
}
Дополнительно заворачивать Функция делает пространство тороидальным. Как я мог исправить этот код так, чтобы он работал как положено. И почему глобальная память не обновляется при каждом изменении задачи? Разве это не должна быть общая память?
Как сказал 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()
чтобы получить результаты. Даже если у вас есть указатель на зону памяти, ваш указатель является указателем на копию этой зоны на стороне хоста. Который, скорее всего, не будет иметь последнюю версию вычисленного вывода устройства.
П.Д .: Я давно создал «живую» игру на OpenCL, и обнаружил, что проще и быстрее сделать это просто создать большой двумерный массив битов (битовая адресация). А затем напишите фрагмент кода без каких-либо веток, который просто анализирует neibours и получает обновленное значение для этой ячейки. Поскольку используется битовая адресация, объем чтения / записи памяти каждым потоком значительно ниже, чем адресация chars / ints / other. Я достиг 33Mcells / sec в очень старом OpenCL HW (nVIDIA 9100M G). Просто чтобы вы знали, что ваш подход if / else, вероятно, не самый эффективный.
В качестве ссылки я приведу здесь мою реализацию игры жизни (ядро 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
};
Здесь вы не сохраняете промежуточные состояния, а только состояние ячейки в конце (жив / смерть). У него нет веток, поэтому он довольно быстрый.