1

我一直在努力创建一个基于 GPU 的康威生命游戏程序。如果您不熟悉它,这里是Wikipedia Page。我创建了一个版本,它通过保留一组值来工作,其中 0 代表死单元格,1 代表活单元格。然后内核简单地写入图像缓冲区数据数组以根据单元格数据绘制图像,然后检查每个单元格的邻居以更新单元格数组以供下一次执行渲染。

但是,一种更快的方法将单元格的值表示为死的负数和活着的正数。该单元格的数量表示它具有的邻居数量加一(使零成为不可能的值,因为我们无法区分 0 和 -0)。然而,这意味着当产生或杀死一个细胞时,我们必须相应地更新它的八个邻居的值。因此,与只需要从相邻内存插槽读取的工作过程不同,该过程必须写入这些插槽。这样做是不一致的,并且输出的数组无效。例如,单元格包含数字,例如 14,表示 13 个邻居,这是一个不可能的值。代码是正确的,因为我在 cpu 上编写了相同的程序并且它按预期工作。测试后,我相信,当任务尝试同时写入内存时,会出现延迟,从而导致某种写入错误。例如,在读取数组数据和设置数据更改的时间之间可能存在延迟,从而导致另一个任务的过程不正确。我尝试过使用信号量和屏障,但刚刚学习了 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 数组。

此外,wrap功能使空间呈环形。我该如何修复此代码以使其按预期工作。为什么全局内存不会随着任务的每次更改而更新?不应该是共享内存吗?

4

2 回答 2

1

正如Sharpneli 在他的回答中所说,您正在从不同的线程读取和写入相同的内存区域,这会产生未定义的行为。

解决方案: 您需要将您newCellMap的数组拆分为 2 个数组,一个用于上一次执行,一个用于存储新值。然后,您需要在每次调用中从主机端更改内核参数,以便oldvalues下一次迭代的 是newvalues上一次迭代的。由于您构建算法的方式,您还需要在运行它之前执行oldvaluesto的复制缓冲区。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:很久以前,我在 OpenCL 上创建了一个“实时”游戏,我发现更简单、更快捷的方法是创建一个大的 2D 位数组(位寻址)。然后编写一段没有任何分支的代码,只分析邻居并获取该单元格的更新值。由于使用了位寻址,每个线程读取/写入的内存量大大低于寻址 chars/ints/other。我在一个非常老的 OpenCL HW (nVIDIA 9100M G) 中达到了 33Mcells/sec。只是为了让您知道您的 if/else 方法可能不是最有效的方法。

于 2013-11-18T09:56:46.093 回答
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
};

在这里,您不保存任何中间状态,只保存最后的单元格状态(生/死)。它没有分支,因此在此过程中非常快。

于 2013-11-18T20:29:35.840 回答