主要内容

使用MEX访问高级CUDA功能

这个例子演示了如何使用MEX文件访问GPU的高级特性。它以这个例子为基础图形处理器上的模板操作。前面的例子使用了Conway的“Game of Life”来演示如何使用在GPU上运行的MATLAB®代码来执行模具操作。这个例子演示了如何使用GPU的两个高级特性:共享内存和纹理内存来进一步提高模板操作的性能。您可以通过在一个MEX文件中编写自己的CUDA代码并从MATLAB调用MEX文件来实现这一点。你可以在MEX文件中找到关于GPU使用的介绍运行包含CUDA代码的mex函数

如上例所定义,在“模板操作”中,输出数组的每个元素都依赖于输入数组的一个小区域。例子包括有限差分、卷积、中值滤波和有限元方法。如果我们假设模板操作是我们工作流程的关键部分,我们可以花时间将它转换成手写的CUDA内核,希望从GPU中获得最大的收益。这个示例使用Conway的“Game of Life”作为我们的模板操作,并将计算移到一个MEX文件中。

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

  • 单元格排列在2D网格中

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

  • 任何有恰好三个邻居的细胞都会在下一步被激活

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

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

因此,本例中的“模板”是每个元素周围3x3的区域。更多细节,查看paralleldemo_gpu_stencil的代码

这个例子允许我们使用子函数:

函数paralleldemo_gpu_mexstencil ()

生成一个随机的初始总体

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

gridSize = 500;numGenerations = 100;初始化网格= (rand(网格大小,网格大小)> .75);持有显示亮度图像(initialGrid);colormap([1 1 1;0 0 0]);标题(“初始网格”);

在MATLAB中创建一个基线的GPU版本

为了获得一个性能基线,我们从中描述的初始实现开始MATLAB实验。这个版本可以在GPU上运行,只需确保初始人口是在GPU上使用gpuArray

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

现在重新运行游戏并测量每一代需要花费的时间。

%这个函数定义了调用每一代的外部循环%做显示。函数网格= callUpdateGrid(网格、gridSize N)gen = 1:N grid = updateGrid(grid, gridSize);结束结束gpuInitialGrid = gpuArray (initialGrid);%保留这个结果,以验证下面每个版本的正确性。expectedResult = callUpdateGrid(gpuInitialGrid, gridSize, numGenerations);gpuBuiltinsTime = gputimeit(@() callUpdateGrid(gpuInitialGrid,gridSize numGenerations));流(GPU上的平均时间:每一代%2.3fms \n,1000 * gpuBuiltinsTime / numGenerations);
GPU上的平均时间:每一代1.528ms

创建使用共享内存的MEX版本

当编写一个CUDA内核版本的模板操作时,我们必须把输入的数据分成块,每个线程块都可以对其进行操作。块中的每个线程都将读取块中的其他线程也需要的数据。最小化读操作数量的一种方法是在处理之前将所需的输入数据复制到共享内存中。这个副本必须包含一些相邻的元素,以便正确计算块的边缘。在《生命的游戏》中,我们的模板只是一个3x3平方的元素,我们需要一个元素边界。例如,对于一个使用3x3块处理的9x9网格,第五个块将操作高亮区域,其中黄色元素是它必须读取的“光晕”。

我们已经将说明这种方法的CUDA代码放入文件中pctdemo_life_cuda_shmem.cu。本文件中的CUDA设备功能操作如下:

  1. 所有线程将输入网格的相关部分复制到共享内存中,包括光环。

  2. 线程之间进行同步,以确保共享内存就绪。

  3. 适合输出网格的线程执行生命游戏计算。

这个文件中的宿主代码使用CUDA运行时API对每一代调用一次CUDA设备函数。它为输入和输出使用两个不同的可写缓冲区。在每次迭代中,MEX-file交换输入和输出指针,因此不需要复制。

为了从MATLAB中调用函数,我们需要一个MEX网关,它从MATLAB中打开输入数组,在GPU上建立一个工作空间,并返回输出。可以在文件中找到MEX网关函数pctdemo_life_mex_shmem.cpp

在调用MEX-file之前,必须使用mexcuda,这需要安装学校网站编译器。您可以使用如下命令将这两个文件编译为一个MEX函数

mexcuda -output pctdemo_life_mex_shmem…pctdemo_life_cuda_shmem.cupctdemo_life_mex_shmem.cpp

它将生成一个名为pctdemo_life_mex_shmem

%使用共享内存的MEX文件计算输出值。的%初始输入值被复制到图形处理器的MEX文件中。grid = pctdemo_life_mex_shmem(initialGrid, numGenerations);gpuMexTime = gputimeit (@ () pctdemo_life_mex_shmem (initialGrid,numGenerations));%打印出平均计算时间并检查结果是否不变。流(`每代平均时间%2.3fms (%1.1fx更快)。\n `,1000 * gpuMexTime / numGenerations gpuBuiltinsTime / gpuMexTime);断言(isequal(网格、expectedResult));
每代平均时间0.055ms(更快27.7倍)。

创建一个使用纹理内存的MEX版本

处理重复读操作问题的第二种方法是使用GPU的纹理内存。当多个线程访问重叠的2-D数据时,纹理访问以一种试图提供良好性能的方式缓存。这正是我们在模板操作中使用的访问模式。

有两个CUDA api可以用来读取纹理存储器。我们选择使用CUDA纹理参考API,所有CUDA设备都支持该API。金宝app绑定到纹理的数组中元素的最大数量是2美元^ {27}$,所以如果输入有更多的元素,纹理方法将不起作用。

说明这种方法的CUDA代码在pctdemo_life_cuda_texture.cu。与前一个版本一样,该文件同时包含主机代码和设备代码。该文件的三个特性使使用纹理记忆在设备功能。

  1. texture变量在mex文件的顶部声明。

  2. CUDA设备函数从纹理参考中获取输入。

  3. MEX-file将纹理引用绑定到输入缓冲区。

在这个文件中,CUDA设备的功能比以前更简单。它只需要进行生命游戏的计算。不需要复制到共享内存或同步线程。

与共享内存版本一样,主机代码使用CUDA运行时API在每一代中调用一次CUDA设备函数。它再次为输入和输出使用两个可写缓冲区,并在每次迭代时交换它们的指针。在每次调用设备函数之前,它将纹理引用绑定到适当的缓冲区。当设备函数执行后,它会解绑定纹理引用。

这个版本有一个MEX网关文件,pctdemo_life_mex_texture.cpp它负责输入和输出数组以及工作空间分配。可以使用如下命令将这些文件构建到单个MEX文件中。

输出pctdemo_life_mex_texture…pctdemo_life_cuda_texture.cupctdemo_life_mex_texture.cpp
%使用带有纹理的MEX文件计算输出值。网格= pctdemo_life_mex_texture(initialGrid, numGenerations);gpuTexMexTime = gputimeit (@ () pctdemo_life_mex_texture (initialGrid,numGenerations));%打印出平均计算时间并检查结果是否不变。流(`每代平均时间%2.3fms (%1.1fx更快)。\n `,1000 * gpuTexMexTime / numGenerations gpuBuiltinsTime / gpuTexMexTime);断言(isequal(网格、expectedResult));
每代平均时间0.025ms(快61.5倍)。

结论

在这个例子中,我们演示了两种不同的方法来处理模板操作的复制输入:显式地将块复制到共享内存中,或者利用GPU的纹理缓存。最好的方法将取决于模板的大小,重叠区域的大小,以及GPU的硬件生成。重要的一点是,您可以将这些方法与MATLAB代码结合使用来优化您的应用程序。

流(第一个使用gpuArray的版本:每一代%2.3fms .\n,1000 * gpuBuiltinsTime / numGenerations);流([使用共享内存的MEX:每代%2.3fms,”(%快1.1 fx)。\ n”1000 * gpuMexTime / numGenerations),gpuBuiltinsTime / gpuMexTime);流([“带有纹理内存的MEX:每代%2.3fms””(%快1.1 fx)。\ n”1000 * gpuTexMexTime / numGenerations]“,gpuBuiltinsTime / gpuTexMexTime);
使用gpuArray的第一个版本:每代1.528毫秒。使用共享内存的MEX:每代0.055毫秒(快27.7倍)。带有纹理存储器的MEX:每代0.025毫秒(快61.5倍)。
结束