如何优化Conway对CUDA的生活游戏?

时间:2011-01-02 22:50:56

标签: c cuda gpgpu

我为Conway的生活游戏编写了这个CUDA内核:

__global__ void gameOfLife(float* returnBuffer, int width, int height) {  
    unsigned int x = blockIdx.x*blockDim.x + threadIdx.x;  
    unsigned int y = blockIdx.y*blockDim.y + threadIdx.y;  
    float p = tex2D(inputTex, x, y);  
    float neighbors = 0;  
    neighbors += tex2D(inputTex, x+1, y);  
    neighbors += tex2D(inputTex, x-1, y);  
    neighbors += tex2D(inputTex, x, y+1);  
    neighbors += tex2D(inputTex, x, y-1);  
    neighbors += tex2D(inputTex, x+1, y+1);  
    neighbors += tex2D(inputTex, x-1, y-1);  
    neighbors += tex2D(inputTex, x-1, y+1);  
    neighbors += tex2D(inputTex, x+1, y-1);  
    __syncthreads();  
    float final = 0;  
    if(neighbors < 2) final = 0;  
    else if(neighbors > 3) final = 0;  
    else if(p != 0) final = 1;  
    else if(neighbors == 3) final = 1;  
    __syncthreads();  
    returnBuffer[x + y*width] = final;  
}

我正在寻找错误/优化。 并行编程对我来说是一个全新的,我不确定我是否能够正确地完成它。

其余的是从输入数组到绑定到CUDA数组的2D纹理inputTex的memcpy。输出从全局内存到主机进行memcpy-ed然后处理。

正如您所看到的,线程会处理单个像素。我不确定这是否是最快的方式,因为一些消息来源建议每个线程执行一行或更多。如果我理解正确NVidia自己说越多线程越好。我很乐意从有实际经验的人那里得到建议。

3 个答案:

答案 0 :(得分:11)

我的两分钱。

整个事情看起来很可能受到多处理器和GPU内存之间通信延迟的限制。您的代码应该采用30-50个时钟周期来执行,并且它会生成至少3个内存访问,如果必需的数据不在缓存中,则每个访问时间超过200个。

使用纹理内存是解决这个问题的好方法,但它不一定是最佳方式。

至少,尝试每个线程一次(水平)做4个像素。全局内存一次可以访问128个字节(只要你有一个warp尝试访问128字节间隔内的任何字节,你也可以在几乎没有额外成本的情况下拉入整个缓存行)。由于warp是32个线程,每个线程在4个像素上工作应该是有效的。

此外,您希望由同一个多处理器处理垂直相邻的像素。原因是相邻行共享相同的输入数据。如果由一个MP处理像素(x = 0,y = 0)并且像素(x = 0,y = 1)由不同的MP处理,则两个MP必须分别发出三个全局存储器请求。如果它们都由同一个MP处理并且结果被正确缓存(隐式或显式),则您只需要总共四个。这可以通过让每个线程在几个垂直像素上工作,或者让blockDim.y> 1来完成。

更一般地说,您可能希望每个32线程的warp加载与MP上可用的内存(16-48 kb,或至少128x128块)一样多的内存,然后处理其中的所有像素窗口。

在2.0之前的计算兼容性设备上,您将需要使用共享内存。在计算兼容性2.0和2.1的设备上,缓存功能得到了很大改善,因此全局内存可能很好。

通过确保每个warp只访问输入像素的每个水平行中的两个缓存行而不是三个缓存行,可以获得一些重要的节省,就像在每个线程4个像素的每个32个线程的天真实现中一样。翘曲。

使用float作为缓冲区类型是没有充分理由的。您不仅最终获得了四倍的内存带宽,而且代码变得不可靠并且容易出错。 (例如,您确定if(neighbors == 3)正常工作,因为您正在比较浮点数和整数吗?)使用unsigned char。更好的是,如果没有定义,请使用uint8_t和typedef它来表示unsigned char。

最后,不要低估实验的价值。很多时候,CUDA代码的性能很难通过逻辑来解释,你不得不求助于调整参数并看看会发生什么。

答案 1 :(得分:3)

看看这个帖子,我们在那里做了很多改进......

http://forums.nvidia.com/index.php?showtopic=152757&st=60

答案 2 :(得分:2)

TL; DR:见:http://golly.sourceforge.net

问题在于,大多数CUDA实施遵循手动计算邻居的大脑死亡理念。这非常慢,任何智能串行CPU实现都将超越它。

进行GoL计算的唯一合理方法是使用查找表 CPU上当前最快的实现使用查找方形4x4 = 16位块来查看未来的2x2单元格。

在此设置中,单元格的布局如下:

 01234567
0xxxxxxxx //byte0
1xxxxxxxx //byte1 
2  etc
3
4
5
6
7

采用一些位移来使4x4块适合一个字,并使用查找表查找该字。查找表也包含单词,这样,结果的4个不同版本可以存储在查找表中,因此您可以最小化在输入和/或输出上完成的位移量。

此外,不同世代交错排列,因此您只需要查看4个相邻的板块,而不是9个。 像这样:

AAAAAAAA 
AAAAAAAA   BBBBBBBB
AAAAAAAA   BBBBBBBB
AAAAAAAA   BBBBBBBB
AAAAAAAA   BBBBBBBB
AAAAAAAA   BBBBBBBB
AAAAAAAA   BBBBBBBB
AAAAAAAA   BBBBBBBB
           BBBBBBBB
//odd generations (A) are 1 pixel above and to the right of B,
//even generations (B) are 1 pixels below and to the left of A.

与愚蠢的计数实施相比,仅此一项就可以实现1000倍以上的加速。

然后优化不计算静态或周期为2的平板。

然后有HashLife,但那是一个完全不同的野兽 HashLife可以在O(log n)时间内生成生命模式,而不是正常实现可以生成的O(n)时间。 这允许您在几秒钟内计算生成:6,366,548,773,467,669,985,195,496,000(6 octillion)。
不幸的是,Hashlife需要递归,因此在CUDA上很难。