技术开发 频道

CUDA入门:GPU的硬件架构

  Shared memory

  目前 CUDA 装置中,每个 multiprocessor 有 16KB 的 shared memory。Shared memory 分成 16 个 bank。如果同时每个 thread 是存取不同的 bank,就不会产生任何问题,存取 shared memory 的速度和存取寄存器相同。不过,如果同时有两个(或更多个) threads 存取同一个 bank 的数据,就会发生 bank conflict,这些 threads 就必须照顺序去存取,而无法同时存取 shared memory 了。

  Shared memory 是以 4 bytes 为单位分成 banks。因此,假设以下的数据:

  __shared__ int data[128];

  那么,data[0] 是 bank 0、data[1] 是 bank 1、data[2] 是 bank 2、…、data[15] 是 bank 15,而 data[16] 又回到 bank 0。由于 warp 在执行时是以 half-warp 的方式执行,因此分属于不同的 half warp 的 threads,不会造成 bank conflict。

  因此,如果程序在存取 shared memory 的时候,使用以下的方式:

  int number = data[base + tid];

  那就不会有任何 bank conflict,可以达到最高的效率。但是,如果是以下的方式:

  int number = data[base + 4 * tid];

  那么,thread 0 和 thread 4 就会存取到同一个 bank,thread 1 和 thread 5 也是同样,这样就会造成 bank conflict。在这个例子中,一个 half warp 的 16 个 threads 会有四个 threads 存取同一个 bank,因此存取 share memory 的速度会变成原来的 1/4。

  一个重要的例外是,当多个 thread 存取到同一个 shared memory 的地址时,shared memory 可以将这个地址的 32 bits 数据「广播」到所有读取的 threads,因此不会造成 bank conflict。例如:

  int number = data[3];

  这样不会造成 bank conflict,因为所有的 thread 都读取同一个地址的数据。

  很多时候 shared memory 的 bank conflict 可以透过修改数据存放的方式来解决。例如,以下的程序:

    data[tid] = global_data[tid];
    ...
    
int number = data[16 * tid];

  会造成严重的 bank conflict,为了避免这个问题,可以把数据的排列方式稍加修改,把存取方式改成:

    int row = tid / 16;
    
int column = tid % 16;
    data[row
* 17 + column] = global_data[tid];
    ...
    
int number = data[17 * tid];

  这样就不会造成 bank conflict 了。

  编者注:share memory在NVIDIA的文档中其实还有不同的叫法,例如PDC(Parallel Data Cache)、PBSM(per-block share memory)。

  Global memory

  由于 multiprocessor 并没有对 global memory 做 cache(如果每个 multiprocessor 都有自己的 global memory cache,将会需要 cache coherence protocol,会大幅增加 cache 的复杂度),所以 global memory 存取的 latency 非常的长。除此之外,前面的文章中也提到过 global memory 的存取,要尽可能的连续。这是因为 DRAM 存取的特性所造成的结果。

  更精确的说,global memory 的存取,需要是 "coalesced"。所谓的 coalesced,是表示除了连续之外,而且它开始的地址,必须是每个 thread 所存取的大小的 16 倍。例如,如果每个 thread 都读取 32 bits 的数据,那么第一个 thread 读取的地址,必须是 16*4 = 64 bytes 的倍数。

  如果有一部份的 thread 没有读取内存,并不会影响到其它的 thread 速行 coalesced 的存取。例如:

    if(tid != 3) {
        
int number = data[tid];
    }

  虽然 thread 3 并没有读取数据,但是由于其它的 thread 仍符合 coalesced 的条件(假设 data 的地址是 64 bytes 的倍数),这样的内存读取仍会符合 coalesced 的条件。

  在目前的 CUDA 1.1 装置中,每个 thread 一次读取的内存数据量,可以是 32 bits、64 bits、或 128 bits。不过,32 bits 的效率是最好的。64 bits 的效率会稍差,而一次读取 128 bits 的效率则比一次读取 32 bits 要显著来得低(但仍比 non-coalesced 的存取要好)。

  如果每个 thread 一次存取的数据并不是 32 bits、64 bits、或 128 bits,那就无法符合 coalesced 的条件。例如,以下的程序:

    struct vec3d { float x, y, z; };
    ...
    __global__
void func(struct vec3d* data, float* output)
    {
        output[tid]
= data[tid].x * data[tid].x +
            data[tid].y
* data[tid].y +
            data[tid].z
* data[tid].z;
    }

  并不是 coalesced 的读取,因为 vec3d 的大小是 12 bytes,而非 4 bytes、8 bytes、或 16 bytes。要解决这个问题,可以使用 __align(n)__ 的指示,例如:

  struct __align__(16) vec3d { float x, y, z; };

  这会让 compiler 在 vec3d 后面加上一个空的 4 bytes,以补齐 16 bytes。另一个方法,是把数据结构转换成三个连续的数组,例如:

    __global__ void func(float* x, float* y, float* z, float* output)
    {
        output[tid]
= x[tid] * x[tid] + y[tid] * y[tid] +
            z[tid]
* z[tid];
    }

  如果因为其它原因使数据结构无法这样调整,也可以考虑利用 shared memory 在 GPU 上做结构的调整。例如:

    __global__ void func(struct vec3d* data, float* output)
    {
        __shared__
float temp[THREAD_NUM * 3];
        
const float* fdata = (float*) data;
        temp[tid]
= fdata[tid];
        temp[tid
+ THREAD_NUM] = fdata[tid + THREAD_NUM];
        temp[tid
+ THREAD_NUM*2] = fdata[tid + THREAD_NUM*2];
        __syncthreads();
        output[tid]
= temp[tid*3] * temp[tid*3] +
            temp[tid
*3+1] * temp[tid*3+1] +
            temp[tid
*3+2] * temp[tid*3+2];
    }

  在上面的例子中,我们先用连续的方式,把数据从 global memory 读到 shared memory。由于 shared memory 不需要担心存取顺序(但要注意 bank conflict 问题,参照前一节),所以可以避开 non-coalesced 读取的问题。

0
相关文章