GPU 编程系列:内核网格与多维数据处理
大家好,欢迎来到 GPU 编程系列的第三集!在这一集中,我们将深入探讨内核网格的概念,并展示如何利用多维网格来处理复杂的数据结构。
3
内核网格的基础
在上期节目中,我们展示了一个简单的向量加法内核。当时,我们启动了一个包含两个线程的线程块。如果我们用六个元素和每个块两个线程来启动内核,生成的内核网格将会是这样的:
- 每个线程被分配一个线程索引(
threadIdx
)和一个块索引(blockIdx
)。 - 当我们在块中运行每个线程时,它将运行代码的一个副本,其中
blockIdx
和threadIdx
的值被设置为与当前执行的线程相匹配。 blockDim
变量代表块的维度,并且在所有线程中保持不变。在我们的例子中,我们将块大小设置为 2,因此blockDim
变量将保存值 2。
多维网格的优势
一些细心的观众可能已经注意到,我们一直在使用 threadIdx.x
和 blockIdx.x
的值。这意味着我们还可以通过传入一个 dim3
变量作为内核参数来运行多达三个维度的网格。
- 二维网格:适用于处理二维数据结构,如图像或矩阵。
- 三维网格:适用于处理三维数据结构,如体数据或时间序列。
多维网格的意义
你可能会好奇,为什么我们需要多个维度?主要原因如下:
- 语法简洁性:多维网格可以让代码更简洁,尤其是在处理多维数据时。
- 边界检查:检查多维数据的边界条件可能会更容易。
- 可读性:以行列形式表达数据时,代码更易读。
- 寄存器使用:在某些边缘情况下,使用多维网格可能会稍微减少寄存器的使用量,但这通常并不太重要。
矩阵乘法的例子
为了更好地理解多维网格的应用,我们可以研究一下方阵乘法内核。矩阵乘法是一种函数,它接收两个矩阵作为输入,并返回另一个矩阵,其余元素是第一个矩阵的行与第二个矩阵的列之间的点积。
内存布局的重要性
在深入代码之前,有一点关于内存布局的知识必须了解。当我们在代码中创建一个二维数组时,计算机仍然会以一维的方式存储它。二维访问只是我们更容易理解的一种抽象概念。
在 CUDA 中,我们能够访问行指针,因此实际上需要自己计算一维索引。这可以通过使用行和列索引来实现。具体来说,我们可以通过将行号乘以矩阵的宽度,再加上列索引来实现这一点。
矩阵乘法的实现
为了运行我们的矩阵乘法内核,我们可以将每个线程分配到输出数组中的一个元素上:
- 计算索引:首先根据当前的线程和块来计算行和列的索引。
- 边界检查:确保不会在矩阵之外进行读写操作。
- 计算点积:创建一个中间变量来存储点积,然后遍历第一个矩阵的行向量和第二个矩阵的列向量,计算点积。
- 保存结果:将结果保存在输出矩阵中。
一维网格的替代方案
正如我之前提到的,我们也可以用一维网格做同样的事情。我们只需要从 X 维度解析出行和列。虽然这会增加一些开销,但与内核完成的其他工作相比,这点开销微不足道。
扩展到第三维度
当我们把数据扩展到第三维度时,也会出现类似的存储模式。它只是在我们添加的哪个数据维度上被简单地摊平了。当你为我们的一维索引想出这个公式时,既然我们已经知道了 X、Y 和 Z 坐标,如果你猜到了以下内容,那你就对了。
练习:多维内核网格
现在理论部分已经讲完,我要给那些想练习运行多维内核网格的人留个作业。这个练习看起来是这样的:
- 输入:三个数组,一个三维数组 A,一个二维数组 B,以及一个一维数组 C。
- 输出:一个三维数组,它是将三个输入数组广播到三维后相加的结果。
请在评论区分享并讨论你的代码。另外,如果你喜欢这个视频,别忘了订阅以获取最新内容,点个赞并分享给你的朋友们。
更多内容关注B站账号:Arthur
https://space.bilibili.com/437018290?spm_id_from=333.33.0.0