我非常了解如何使用cudaMalloc()
和cudaMemcpy()
分配和复制线性内存。但是,当我想使用CUDA函数来分配和复制2D或3D矩阵时,我常常会被各种参数所迷惑,特别是关于在处理2D / 3D数组时总是存在的倾斜指针。文档很适合提供一些如何使用它们的例子,但它假设我熟悉填充和音高的概念,我不是。
我通常最终会调整我在文档中或网络上的其他地方找到的各种示例,但后面的盲目调试非常痛苦,所以我的问题是:
什么是球场?我该如何使用它?如何在CUDA中分配和复制2D和3D数组?
答案 0 :(得分:80)
以下是关于CUDA中的指针和填充的说明。
首先,让我们从存在非线性内存的原因入手。使用cudaMalloc分配内存时,结果就像使用malloc的分配一样,我们有一个指定大小的连续内存块,我们可以在其中放入任何我们想要的东西。如果我们想要分配10000浮点数的向量,我们只需:
float* myVector;
cudaMalloc(&myVector, 10000*sizeof(float));
然后通过经典索引访问myVector的第i个元素:
float element = myVector[i];
如果我们想要访问下一个元素,我们只需:
float next_element = myvector[i+1];
它的工作非常精细,因为访问第一个元素旁边的元素是(因为我不知道而且我现在不想要的原因)便宜。
当我们将内存用作2D数组时,情况会有所不同。假设我们的10000浮点矢量实际上是一个100x100阵列。我们可以使用相同的cudaMalloc函数来分配它,如果我们想要读取第i行,我们会这样做:
float* myArray;
cudaMalloc(&myArray, 10000*sizeof(float));
int row[100]; // number of columns
for (int j=0; j<100; ++j)
row[j] = myArray[i*100+j];
所以我们必须从myArray + 100 * i读取内存到myArray + 101 * i-1。它将占用的内存访问操作数取决于此行占用的内存字数。存储器字中的字节数取决于实现。为了在读取单行时最小化内存访问次数,我们必须确保在单词的开头开始行,因此我们必须为每一行填充内存,直到新行开始。
填充数组的另一个原因是CUDA中的银行机制,涉及共享内存访问。当阵列在共享存储器中时,它被分成几个存储体。两个CUDA线程可以同时访问它,前提是它们不能访问属于同一存储体的存储器。由于我们通常希望并行处理每一行,因此我们可以确保通过将每一行填充到新银行的开头来模拟访问它。
现在,我们将使用cudaMallocPitch代替使用cudaMalloc分配2D数组:
size_t pitch;
float* myArray;
cudaMallocPitch(&myArray, &pitch, 100*sizeof(float), 100); // width in bytes by height
请注意,此处的音高是函数的返回值:cudaMallocPitch检查系统应该是什么,并返回适当的值。 cudaMallocPitch的作用如下:
最后,我们通常分配的内存超过了必要的内存,因为每行现在都是音高的大小,而不是w*sizeof(float)
的大小。
但是现在,当我们想要访问列中的元素时,我们必须这样做:
float* row_start = (float*)((char*)myArray + row * pitch);
float column_element = row_start[column];
两个连续列之间的字节偏移量不能再从数组的大小中推断出来,这就是为什么我们要保持cudaMallocPitch返回的音高。并且由于音高是填充大小的倍数(通常是字大小和行大小最大),因此效果很好。耶。
现在我们知道如何创建和访问由cudaMallocPitch创建的数组中的单个元素,我们可能希望将其整个部分复制到其他内存中,也可以复制到其他内存中。
让我们说我们想要在我们的主机上使用malloc分配的100x100数组中复制我们的数组:
float* host_memory = (float*)malloc(100*100*sizeof(float));
如果我们使用cudaMemcpy,我们将复制用cudaMallocPitch分配的所有内存,包括每行之间的填充字节。我们必须做的是避免填充内存是逐个复制每一行。我们可以手动完成:
for (size_t i=0; i<100; ++i) {
cudaMemcpy(host_memory[i*100], myArray[pitch*i],
100*sizeof(float), cudaMemcpyDeviceToHost);
}
或者我们可以告诉CUDA API,我们只想从我们为它方便的填充字节分配的内存中获得有用的内存,所以如果它可以自动处理它自己的混乱,那将是非常好的的确,谢谢。这里输入cudaMemcpy2D:
cudaMemcpy2D(host_memory, 100*sizeof(float)/*no pitch on host*/,
myArray, pitch/*CUDA pitch*/,
100*sizeof(float)/*width in bytes*/, 100/*heigth*/,
cudaMemcpyDeviceToHost);
现在副本将自动完成。它将复制宽度中指定的字节数(此处为:100xsizeof(float)),高度时间(此处为100),每次跳转到下一行时跳过 pitch 字节。请注意,我们仍然必须提供目标内存的音高,因为它也可以填充。这里不是,所以音高等于非填充数组的音高:它是一行的大小。另请注意,memcpy函数中的width参数以字节表示,但height参数以元素数表示。这是因为副本的完成方式,就像我上面写的手册一样:宽度是一行中每个副本的大小(内存中连续的元素),高度是此操作必须的次数完成。 (作为一名物理学家,这些单位的不一致使我非常恼火。)
3D阵列实际上与2D阵列没有什么不同,没有包含额外的填充。 3D数组只是填充行的2D 经典数组。这就是为什么在分配3D数组时,您只得到一个音高,即沿着一行连续点之间的字节数差异。如果要访问沿深度维度的连续点,可以安全地将音高乘以列数,从而为slicePitch提供。
用于访问3D内存的CUDA API与用于2D内存的CUDA API略有不同,但想法是一样的: