Memory CUDA合并访问全局内存

Memory CUDA合并访问全局内存,memory,cuda,copy,coalescing,Memory,Cuda,Copy,Coalescing,我读过《CUDA编程指南》,但错过了一件事。假设我在全局内存中有一个32位int的数组,我想通过合并访问将它复制到共享内存中。 全局数组有从0到1024的索引,假设我有4个块,每个块有256个线程 __shared__ int sData[256]; 何时执行合并访问 一, 全局内存中的地址从0复制到255,每个地址由warp中的32个线程复制,所以这里可以吗 二, 如果某个索引不是32的倍数,那么它不是合并的?地址不对?这是正确的吗?可以合并访问的规则有点复杂,并且随着时间的推移它们已经发生

我读过《CUDA编程指南》,但错过了一件事。假设我在全局内存中有一个32位int的数组,我想通过合并访问将它复制到共享内存中。 全局数组有从0到1024的索引,假设我有4个块,每个块有256个线程

__shared__ int sData[256];
何时执行合并访问

一,

全局内存中的地址从0复制到255,每个地址由warp中的32个线程复制,所以这里可以吗

二,


如果某个索引不是32的倍数,那么它不是合并的?地址不对?这是正确的吗?

可以合并访问的规则有点复杂,并且随着时间的推移它们已经发生了变化。每一种新的CUDA体系结构在结合方面都更加灵活。我想说一开始不要担心。相反,以最方便的方式访问内存,然后查看CUDA探查器的说明。

如果您打算使用1D网格和线程几何体,那么您的示例是正确的。我认为您打算使用的索引是
[blockIdx.x*blockDim.x+threadIdx.x]

对于#1,warp中的32个线程“同时”执行该指令,因此它们的请求是顺序的,并与128B(32 x 4)对齐,我相信它们在Tesla和Fermi体系结构中都结合在一起

对于#2,它有点模糊。如果
someIndex
为1,则它不会在一个warp中合并所有32个请求,但可能会执行部分合并。我相信费米设备将把对扭曲中线程1-31的访问合并为128B连续内存段的一部分(而没有线程需要的前4B被浪费掉了)。我认为特斯拉体系结构设备会由于未对准而使其成为非平衡访问,但我不确定


例如,使用
someIndex
作为8,特斯拉将拥有32B对齐地址,费米可能将它们分组为32B、64B和32B。但底线是,取决于
someIndex
的值和体系结构,所发生的事情是模糊的,不一定会很糟糕。

您在1处的索引错误(或者故意奇怪,似乎是错误的),一些块访问每个线程中的相同元素,因此无法在这些块中合并访问

证明:

例如:

Grid = dim(2,2,0)

t(blockIdx.x, blockIdx.y)

//complete block reads at 0
t(0,0) -> sData[threadIdx.x] = gData[0];
//complete block reads at 2
t(0,1) -> sData[threadIdx.x] = gData[2];
//definetly coalesced
t(1,0) -> sData[threadIdx.x] = gData[threadIdx.x];
//not coalesced since 2 is no multiple of a half of the warp size = 16
t(1,1) -> sData[threadIdx.x] = gData[threadIdx.x + 2];
因此,如果一个区块合并在一起,这是一个“幸运”游戏,因此通常

但是,合并内存读取规则对于较新的cuda版本不像以前那样严格。
但对于兼容性问题,如果可能的话,您应该尝试优化内核以获得最低的cuda版本

以下是一些很好的来源:


您想要的最终取决于输入数据是1D还是2D数组,以及栅格和块是1D还是2D。最简单的情况是1D:

shmem[threadIdx.x] = gmem[blockDim.x * blockIdx.x + threadIdx.x];
这是联合的。我使用的经验法则是,变化最快的坐标(threadIdx)作为块偏移量(blockDim*blockIdx)的偏移量添加。最终结果是块中线程之间的索引跨距为1。如果步幅变大,则会失去凝聚

简单的规则(在Fermi和更高版本的GPU上)是,如果一个扭曲中所有线程的地址都位于同一个对齐的128字节范围内,那么将产生一个内存事务(假设为加载启用了缓存,这是默认情况)。如果它们落在两个对齐的128字节范围内,则会产生两个内存事务,以此类推

在GT2xx和更早的GPU上,它变得更加复杂。但您可以在编程指南中找到这方面的详细信息

其他示例:

未合并:

shmem[threadIdx.x] = gmem[blockDim.x + blockIdx.x * threadIdx.x];
未合并,但在GT200及更高版本上也不太糟糕:

stride = 2;
shmem[threadIdx.x] = gmem[blockDim.x * blockIdx.x + stride * threadIdx.x];
完全没有合并:

stride = 32;
shmem[threadIdx.x] = gmem[blockDim.x * blockIdx.x + stride * threadIdx.x];
合并、二维网格、1D块:

int elementPitch = blockDim.x * gridDim.x;
shmem[threadIdx.x] = gmem[blockIdx.y * elementPitch + 
                          blockIdx.x * blockDim.x + threadIdx.x]; 
合并的二维网格和块:

int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int elementPitch = blockDim.x * gridDim.x;
shmem[threadIdx.y * blockDim.x + threadIdx.x] = gmem[y * elementPitch + x];

这不能说,因为他的索引是错误的或非常奇怪的,看我的回答嗯,你是对的,很好的捕捉@Hlavson,根据你的问题,我假设你有一个一维网格和一维螺纹几何体。因此,您需要使用
[blockIdx.x*blockDim.x+threadIdx.x]
进行索引。恐怕这个答案是完全错误的。线程编号是块中的主列编号,并且所有线程都有threadIdx.x乘以跨步(blockIdx.x)。在第一种情况下,第一个块将发生完全oalescing,但之后不会发生。第二种情况与第一种情况相同,但有一个偏移量。很抱歉,它不是。对于案例1。如果您有一个1D块,那么第一个块的读取跨距为1个字,这将合并。第二个块的读取步幅为2,不会合并,第三个块的读取步幅为3,依此类推。具有1D块的情况#1的等效公式为
threadIdx.x*blockIdx.x+gridDim.x
。这永远不会完全融合。案例2只是案例1,有一个额外的偏移量。对不起,我不知道你在说什么。在任何块中,两个线程之间的唯一区别是threadIdx.x中的差异;所以在一个扭曲中,如果它开始对齐,它会合并,如果它没有,它会做奇怪的事情。我同意他的问题中的索引是错误的——我在评论中提到了这一点。但这并不是我们忽略眼前这个问题的理由,这个问题是关于内存访问何时合并,何时不合并。除了网格中的第一个块,这两个块都不能合并。线程按列的主要顺序进行编号。添加了更多的精确性和示例。
int elementPitch = blockDim.x * gridDim.x;
shmem[threadIdx.x] = gmem[blockIdx.y * elementPitch + 
                          blockIdx.x * blockDim.x + threadIdx.x]; 
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int elementPitch = blockDim.x * gridDim.x;
shmem[threadIdx.y * blockDim.x + threadIdx.x] = gmem[y * elementPitch + x];