文章

CUDA优化:Bank Conflict

CUDA优化:Bank Conflict

使用到的测试代码:github – cuda_perf

1. Bank Conflicts (Shared Memory)

针对Shared Memory的访问,CUDA使用bank机制,将shared memory的访问(读/写)映射到不同的bank,以实现并行访问。bank以4字节为单位,共32个bank。这样,一个时钟周期内,可以并行访问32个不同的bank,即访问128字节的数据。映射公式bank index = (address /4) % 32

图示transaction

1
2
3
4
5
6
7
Thread(在CUDA Core中)
    ↓
  访问Shared Memory
    ↓
[Bank系统处理] ← Transaction在这里发生
    ↓
返回数据到Thread

举例:warp中定义的shared memory如何映射到banks

1
__shared__ float s[64];

如上变量,其映射如下:

shared-memory-bank-map

在一次transaction的时候,如果,当warp中的不同线程访问到同一个bank中的不同地址时,就会产生Bank Conflicts,导致访问串行化:需要分成多次transaction。有N个线程访问同一个bank,称为N-way Bank Conflicts

Bank Conflicts影响仅限一个warp内的一次transaction,且内存为shared memory

如下情况,会产生Bank Conflicts

  • 一次transaction中,warp中的多个线程,访问同一个bank中的不同地址;
  • 一次transaction中,warp中的多个线程,访问shared memory的下一个128字节,被映射到同一个bank。此时也是属于上一种情况:同一个bank中的不同地址。

如下情况,Bank Conflicts不会产生:

  • warp中的线程,访问地址唯一对应到bank簇的每个bank,不论是顺序,还是错位;
  • warp中的多个线程,访问同一个bank中的相同地址–使用boardcast分发相同地址数据到多个线程;
  • warp中的线程,单个线程一次访问多个bank,但其他线程不访问这些bank。此时,生成多次transaction

2. Bank Conflicts 示例

以下示例来自博客Notes About Nvidia GPU Shared Memory Banks

如下示例,产生32路Bank Conflicts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__global__ void all_conflicts() {
  __shared__ float s[32][32];
  [[maybe_unused]] int warp_id = threadIdx.y;
  int lane_id = threadIdx.x; // thread 在 warp 中的 id

  float *ptr = &s[lane_id][0];
  int addr = (int)(uintptr_t)ptr & 0xFFFF;
  [[maybe_unused]] float r1; // 声明输出变量

  for (int j = 0; j < num_iters; j++) { // num_iters 定义为 100'000
    asm volatile("ld.volatile.shared.f32 %0, [%1];" : "=f"(r1) : "r"(addr));
  }
}

// launched withall_conflicts<<<1, dim3(32, 8)>>>();
// Gride size: 1(即只有一个Block)
// Block size: dim3(32, 8)(即有8个warp,每个warp 32个线程)

bank-conflict-example

使用Nsight Compute分析bank conflicts(Metrics full),分析步骤见使用Nsight Compute分析Bank Conflict

2.1. conflict free 代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__global__ void conflict_free_kernel() {
  __shared__ float s[8][32];
  int warp_id = threadIdx.y;
  int lane_id = threadIdx.x;

  float *ptr = &s[warp_id][lane_id];
  int addr = (int)(uintptr_t)ptr & 0xFFFF;
  [[maybe_unused]] float r1;  // 声明输出变量

  for (int j = 0; j < num_iters; j++) {
    asm volatile("ld.volatile.shared.f32 %0, [%1];" : "=f"(r1) : "r"(addr));
  }
}

// conflict_free_kernel<<<1, dim3(32, 8)>>>();
// Gride size: 1(即只有一个Block)
// Block size: dim3(32, 8)(即有8个warp,每个warp 32个线程)

3. 矢量读写指令

使用矢量指令ld.shared.v4可以读取4个连续的32位数据。如下代码,一个线程读取s[4*i], s[4*i+1], s[4*i+2], s[4*i+3](分为四个transaction)。会产生四路Bank Conflicts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__global__ void vectorized_loads() {
    __shared__ float sh[8][128];

    int warp_id = threadIdx.y;
    int lane_id = threadIdx.x;

    float4* ptr = reinterpret_cast<float4*>(&sh[warp_id][lane_id * 4]);
    int addr = (int)ptr & 0xFFFF;

    float4 r;
    for (int j = 0; j < num_iters; j++) {
        asm volatile ("ld.volatile.shared.v4.f32 {%0,%1,%2,%3}, [%4];"
                        : "=f"(r.x), "=f"(r.y), "=f"(r.z), "=f"(r.w)
                        : "r"(addr));
    }
}

vectorized-loads

上图只给出了一半的线程访问情况。编号(Lane)为0的线程与编号为8的线程,访问的shared memory中的数据映射到了同一个bank 0;同时,编号为16,以及24,同样映射到了bank 0。 不过,由于其每个线程一次访问4个32位数据,其平均访问时间折算下来,与32位加载相当。

要想避免Bank Conflicts,可以错开(interleave)冲突的线程访问的顺序,比如:

  • 线程0:s[0] -> s[1] -> s[2] -> s[3]
  • 线程8:s[33] -> s[34] -> s[35] -> s[32]

3. 避免 Bank Conflicts 的方法

3.1. Padding

warp内多个线程访问同一bank会引发冲突,导致串行化访问。 通过在二维共享内存数组的列数上 +1 padding,可打破冲突模式。示意图:

padding-example

示例代码:

1
2
3
4
5
6
7
__shared__ float sData[BLOCKSIZE][BLOCKSIZE + 1]; // +1 避免bank冲突

int x = threadIdx.x;
int y = threadIdx.y;
sData[x][y] = matrix[y * col + x];
__syncthreads();
matrixTest[y * col + x] = sData[x][y];

参考资料

学习资料

本文由作者按照 CC BY 4.0 进行授权