主要内容

模板对GPU的操作

这个例子使用了Conway的“Game of Life”来演示如何使用GPU执行模板操作。

许多数组操作都可以表示为“模板操作”,其中输出数组的每个元素都依赖于输入数组的一个小区域。例子包括有限差分、卷积、中值滤波和有限元方法。这个例子使用Conway的“Game of Life”来演示在GPU上运行模板操作的两种方法,从Cleve Moler电子书中的代码开始MATLAB实验

“生活游戏”遵循一些简单的规则:

  • 单元格按2D网格排列

  • 在每一步中,每个细胞的命运都是由它最邻近的八个细胞的活力决定的

  • 任何有三个邻居的细胞在下一步就会有生命

  • 一个有两个邻居的活细胞在下一步仍然是活的

  • 所有其他单元格(包括有三个以上邻居的单元格)都会在下一步死亡或保持空状态

因此,在本例中,“模板”是每个元素周围3x3的区域。下面是一些单元格更新的例子:

这个例子是一个允许使用嵌套函数的函数:

函数paralleldemo_gpu_stencil ()

生成一个随机初始总体

最初的细胞群是在2D网格上创建的,大约有25%的位置是活的。

gridSize = 500;numGenerations = 100;initialGrid = (rand(gridSize,gridSize) > .75);gpu = gpuDevice ();%绘制初始网格持有显示亮度图像(initialGrid);Colormap ([1 1 1;0 0.5 0]);标题(“初始网格”);

玩生命的游戏

电子书MATLAB实验提供可用于比较的初始实现。这个版本是完全矢量化的,每代更新一次网格中的所有单元格。

函数updateGrid(X, N) p = [1 1:N-1];q = [2:N N];数一下八个邻居中有多少人还活着。邻居= X(:,p) + X(:,q) + X(p,:) + X(q,:) +...X(p,p) + X(q,q) + X(p,q) + X(q,p);一个活的细胞有两个活的邻居,或者任何有三个活着的邻居,是活着的下一步。。X = (X & (neighbors == 2))) | (neighbors == 3);结束网格= initialGrid;%循环每一代更新网格并显示它= 1:numGenerations grid = updateGrid(grid, gridSize);显示亮度图像(网格);标题(num2str(代));drawnow;结束

现在重新运行游戏并衡量每一代所花费的时间。

网格= initialGrid;计时器=抽搐();= 1:numGenerations grid = updateGrid(grid, gridSize);结束cpuTime = toc(计时器);流('平均CPU时间:%2.3fms每代' \n'...1000 * cpuTime / numGenerations);
平均CPU时间:每代11.323ms。

保留这个结果以验证下面每个版本的正确性。

expectedResult =网格;

将生命游戏转换到GPU上运行

为了在GPU上运行Game of Life,初始人口被发送到GPU使用gpuArray.算法保持不变。请注意,等待(GPUDevice)用于确保GPU在计时器停止前已经完成计算。只有在准确计时时才需要这样做。

网格= gpuArray (initialGrid);计时器=抽搐();= 1:numGenerations grid = updateGrid(grid, gridSize);结束等待(gpu);只需要确保准确的计时gpuSimpleTime = toc(计时器);%打印平均计算时间,并检查结果是否不变。流([“GPU的平均使用时间:%2.3fms /代”...”(%快1.1 fx)。\ n”],...1000 * gpuSimpleTime / numGenerations cpuTime / gpuSimpleTime);断言(isequal(网格、expectedResult));
使用GPU的平均时间:1.655毫秒/代(快6.8倍)。

为GPU创建一个元素版本

看一下updateGrid函数时,显然相同的操作分别应用于每个网格位置。这表明,arrayfun可以用来做评估。然而,每个细胞都需要知道它的八个邻居,这打破了元素的独立性。每个元素都需要能够访问完整的网格,同时也能够独立工作。

解决方案是使用嵌套函数。嵌套函数,即使是与arrayfun,可以访问在其父函数中声明的变量。这意味着每个单元格都可以从之前的时间步骤中读取整个网格并在其中建立索引。

网格= gpuArray (initialGrid);函数X = updateParentGrid(row, col, N)考虑边界效应rowU = max(第1行);rowD = min (N,行+ 1);科尔= max(1、col-1);colR = min (N + 1)上校;%计算邻居邻居...= grid(rowU,colL) + grid(row,colL) + grid(rowD,colL)...+ grid(rowU,col) + grid(rowD,col)...+ grid(rowU,colR) + grid(row,colR) + grid(rowD,colR);一个活的细胞有两个活的邻居,或者任何有三个活着的邻居,是活着的下一步。。X = (grid(row,col) & (neighbors == 2))) | (neighbors == 3);结束计时器=抽搐();行= gpuArray。冒号(gridSize) ';关口= gpuArray。结肠(1、gridSize);= 1:numGenerations grid = arrayfun(@updateParentGrid, rows, cols, gridSize);结束等待(gpu);只需要确保准确的计时gpuArrayfunTime = toc(计时器);%打印平均计算时间,并检查结果是否不变。流([“使用GPU arrayfun的平均时间:每代%2.3fms”...”(%快1.1 fx)。\ n”],...1000 * gpuArrayfunTime / numGenerations cpuTime / gpuArrayfunTime);断言(isequal(网格、expectedResult));
使用GPU arrayfun的平均时间:每代0.795ms(快14.2倍)。

注意,我们还使用了的另一个新特性arrayfun:维扩张。我们只需要传递行和列向量,它们将自动扩展到完整的网格中。其效果就像我们调用:

[关口,行]= meshgrid(关口、行);

作为arrayfun调用。这节省了CPU内存和GPU内存之间的一些计算和数据传输。

结论

在这个例子中,一个简单的模板操作,Conway的“Game of Life”,已经在GPU上使用arrayfun和在父函数中声明的变量。该技术可用于实现一系列模板操作,包括有限元算法、卷积和过滤器。它还可以用于访问父函数中定义的查找表中的元素。

流('CPU:每代%2.3fms .\n'...1000 * cpuTime / numGenerations);流(简单GPU:每代%2.3fms (%1.1fx快)\n...1000 * gpuSimpleTime / numGenerations cpuTime / gpuSimpleTime);流(Arrayfun GPU: %2.3fms / generation (%1.1fx faster).\n'...1000 * gpuArrayfunTime / numGenerations cpuTime / gpuArrayfunTime);
CPU:每代11.323ms。简单GPU: 1.655毫秒/代(快6.8倍)。Arrayfun GPU:每代0.795ms(速度14.2倍)。
结束