|
11 | 11 | - [X] sgemm_t_8x8_sliced_k_f32x4_bcf_dbuf_kernel (bank conflicts free, double buffers)
|
12 | 12 | - [X] PyTorch bindings
|
13 | 13 |
|
| 14 | +## 共享内存 Bank Conflicts |
| 15 | + |
| 16 | +含义:在访问shared memory时,因多个线程读写同一个Bank中的不同数据地址时,导致shared memory 并发读写 退化 成顺序读写的现象叫做Bank Conflict; |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +SM调度单位为一个warp(一个warp内32个Thread),shared_memory 可以 被一个warp中的所有(32个)线程进行访问,shared_memory 映射到大小相等的32个Bank上,Bank的数据读取带宽为32bit / cycle (4 bytes),因此,主要需要考虑一个Warp内32线程的访问共享内存时的bank冲突。 |
| 21 | +对于多个线程读取同一个Bank数据时(不同地址),硬件把内存读写请求,拆分成 conflict-free requests,进行顺序读写,此时将会触发多次内存事务。特别地,当一个warp中的所有线程读写同一个地址时,会触发broadcast机制,此时不会退化成顺序读写。上面提到触发broadcast机制的条件是all threads acess same address,但在翻阅cuda-c-programming-guide以及最新版本的[NVProfGuide](https://docs.nvidia.com/nsight-compute/ProfilingGuide/index.html) 时,发现只要是多个thread 读写就会触发broadcast(不需要All)。 |
| 22 | + |
| 23 | +- 多个线程读同一个数据时,仅有一个线程读,然后broadcast到其他线程 |
| 24 | +- 多个线程写同一个数据时,仅会有一个线程写成功 |
| 25 | + |
| 26 | +NVIDIA的[文章](https://developer.nvidia.com/blog/using-shared-memory-cuda-cc/)中指出,我们还可以通过 `cudaDeviceSetSharedMemConfig()` 函数设置默认Bank Size(默认为4 bytes)来避免bank conflicts,可设置为cudaSharedMemBankSizeFourByte或者cudaSharedMemBankSizeEightByte。对于某些场景来说,设置cudaSharedMemBankSizeEightByte或许更加合适,比如使用double数据类型时。 |
| 27 | + |
| 28 | +```C |
| 29 | +cudaDeviceSetSharedMemConfig(cudaSharedMemBankSizeEightByte); |
| 30 | +``` |
| 31 | +
|
| 32 | +## 双缓冲 Double Buffers |
| 33 | +
|
| 34 | +本仓库实现的SGEMM Double Buffers策略如下:1)主循环从bk = 1 开始,第一次数据加载在主循环之前,最后一次计算在主循环之后,这是pipeline 的特点决定的;2)由于计算和下一次访存使用的Shared Memory不同,因此主循环中每次循环只需要一次__syncthreads()即可,对比非double buffers版本,总共节省了 ((K + BK - 1) / BK) - 1 次block内的同步操作。比如,bk=1时,FFMA计算使用的是s_a[0]和s_b[0],因此,和s_a[1]和s_b[1]的加载是没有依赖关系的。FFMA计算,从global内存到s_a[1]和s_b[1]和HFMA计算可以并行。s_a[1]和s_b[1]用于加载下一块BK需要的数据到共享内存;3)由于GPU不能向CPU那样支持乱序执行,主循环中需要先将下一次循环计算需要的Gloabal Memory中的数据load 到寄存器,然后进行本次计算,之后再将load到寄存器中的数据写到Shared Memory,这样在LDG指令向Global Memory做load时,不会影响后续HFMA及其它运算指令的 launch 执行,也就达到了Double Buffers的目的。 |
| 35 | +
|
| 36 | +```C |
| 37 | + // 1)主循环从bk = 1 开始,第一次数据加载在主循环之前,最后一次计算在主循环之后,这是pipeline 的特点决定的; |
| 38 | + // 2)由于计算和下一次访存使用的Shared Memory不同,因此主循环中每次循环只需要一次__syncthreads()即可 |
| 39 | + // 3)由于GPU不能向CPU那样支持乱序执行,主循环中需要先将下一次循环计算需要的Gloabal Memory中的数据load |
| 40 | + // 到寄存器,然后进行本次计算,之后再将load到寄存器中的数据写到Shared Memory,这样在LDG指令向Global |
| 41 | + // Memory做load时,不会影响后续FFMA及其它运算指令的 launch 执行,也就达到了Double Buffering的目的。 |
| 42 | + |
| 43 | + // bk = 0 is loading here, buffer 0 |
| 44 | +
|
| 45 | + { |
| 46 | + int load_a_gmem_k = load_a_smem_k; |
| 47 | + int load_a_gmem_addr = load_a_gmem_m * K + load_a_gmem_k; |
| 48 | + int load_b_gmem_k = load_b_smem_k; |
| 49 | + int load_b_gmem_addr = load_b_gmem_k * N + load_b_gmem_n; |
| 50 | + FLOAT4(r_load_a[0]) = FLOAT4(a[load_a_gmem_addr]); |
| 51 | + FLOAT4(r_load_b[0]) = FLOAT4(b[load_b_gmem_addr]); |
| 52 | +
|
| 53 | + s_a[0][load_a_smem_k + 0][load_a_smem_m] = r_load_a[0]; |
| 54 | + s_a[0][load_a_smem_k + 1][load_a_smem_m] = r_load_a[1]; |
| 55 | + s_a[0][load_a_smem_k + 2][load_a_smem_m] = r_load_a[2]; |
| 56 | + s_a[0][load_a_smem_k + 3][load_a_smem_m] = r_load_a[3]; |
| 57 | + FLOAT4(s_b[0][load_b_smem_k][load_b_smem_n]) = FLOAT4(r_load_b[0]); |
| 58 | + } |
| 59 | + // Without this synchronization, accuracy may occasionally be abnormal. |
| 60 | + __syncthreads(); |
| 61 | +
|
| 62 | + // bk start from 1,需要注意的是,虽然 bk 从 1 开始,但实际上 bk=1时,使用的是 |
| 63 | + // 第0块BK中的数据(已经加载到共享内存s_a[0]和s_b[0]);bk=2时,实际计算的是第1块 |
| 64 | + // BK中的数据。其余以此类推,这个循环结束后,剩下最后一块BK大小的数据需要计算。 |
| 65 | + for (int bk = 1; bk < (K + BK - 1) / BK; bk++) { |
| 66 | +
|
| 67 | + int smem_sel = (bk - 1) & 1; |
| 68 | + int smem_sel_next = bk & 1; |
| 69 | +
|
| 70 | + int load_a_gmem_k = bk * BK + load_a_smem_k; |
| 71 | + int load_a_gmem_addr = load_a_gmem_m * K + load_a_gmem_k; |
| 72 | + int load_b_gmem_k = bk * BK + load_b_smem_k; |
| 73 | + int load_b_gmem_addr = load_b_gmem_k * N + load_b_gmem_n; |
| 74 | + FLOAT4(r_load_a[0]) = FLOAT4(a[load_a_gmem_addr]); |
| 75 | + FLOAT4(r_load_b[0]) = FLOAT4(b[load_b_gmem_addr]); |
| 76 | +
|
| 77 | + #pragma unroll |
| 78 | + for (int tk = 0; tk < BK; tk++) { |
| 79 | + FLOAT4(r_comp_a[0]) = FLOAT4(s_a[smem_sel][tk][ty * TM / 2 ]); |
| 80 | + FLOAT4(r_comp_a[4]) = FLOAT4(s_a[smem_sel][tk][ty * TM / 2 + BM / 2]); |
| 81 | + FLOAT4(r_comp_b[0]) = FLOAT4(s_b[smem_sel][tk][tx * TN / 2 ]); |
| 82 | + FLOAT4(r_comp_b[4]) = FLOAT4(s_b[smem_sel][tk][tx * TN / 2 + BN / 2]); |
| 83 | +
|
| 84 | + #pragma unroll |
| 85 | + for (int tm = 0; tm < TM; tm++) { |
| 86 | + #pragma unroll |
| 87 | + for (int tn = 0; tn < TN; tn++) { |
| 88 | + // r_c[tm][tn] += r_comp_a[tm] * r_comp_b[tn]; |
| 89 | + r_c[tm][tn] = __fmaf_rn(r_comp_a[tm], r_comp_b[tn], r_c[tm][tn]); |
| 90 | + } |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + // 对比非double buffers版本,此处不需要__syncthreads(),总共节省了 |
| 95 | + // ((K + BK - 1) / BK) - 1 次block内的同步操作。比如,bk=1时,HFMA计算 |
| 96 | + // 使用的是s_a[0]和s_b[0],因此,和s_a[1]和s_b[1]的加载是没有依赖关系的。 |
| 97 | + // 从global内存到s_a[1]和s_b[1]和HFMA计算可以并行。s_a[1]和s_b[1]用于 |
| 98 | + // 加载下一块BK需要的数据到共享内存。 |
| 99 | + s_a[smem_sel_next][load_a_smem_k + 0][load_a_smem_m] = r_load_a[0]; |
| 100 | + s_a[smem_sel_next][load_a_smem_k + 1][load_a_smem_m] = r_load_a[1]; |
| 101 | + s_a[smem_sel_next][load_a_smem_k + 2][load_a_smem_m] = r_load_a[2]; |
| 102 | + s_a[smem_sel_next][load_a_smem_k + 3][load_a_smem_m] = r_load_a[3]; |
| 103 | + FLOAT4(s_b[smem_sel_next][load_b_smem_k][load_b_smem_n]) = FLOAT4(r_load_b[0]); |
| 104 | +
|
| 105 | + __syncthreads(); |
| 106 | + } |
| 107 | + |
| 108 | + // 计算剩下最后一块BK |
| 109 | + #pragma unroll |
| 110 | + for (int tk = 0; tk < BK; tk++) { |
| 111 | + FLOAT4(r_comp_a[0]) = FLOAT4(s_a[1][tk][ty * TM / 2 ]); |
| 112 | + FLOAT4(r_comp_a[4]) = FLOAT4(s_a[1][tk][ty * TM / 2 + BM / 2]); |
| 113 | + FLOAT4(r_comp_b[0]) = FLOAT4(s_b[1][tk][tx * TN / 2 ]); |
| 114 | + FLOAT4(r_comp_b[4]) = FLOAT4(s_b[1][tk][tx * TN / 2 + BN / 2]); |
| 115 | +
|
| 116 | + #pragma unroll |
| 117 | + for (int tm = 0; tm < TM; tm++) { |
| 118 | + #pragma unroll |
| 119 | + for (int tn = 0; tn < TN; tn++) { |
| 120 | + // r_c[tm][tn] += r_comp_a[tm] * r_comp_b[tn]; |
| 121 | + r_c[tm][tn] = __fmaf_rn(r_comp_a[tm], r_comp_b[tn], r_c[tm][tn]); |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | +``` |
| 126 | + |
| 127 | +## 参考文献 |
| 128 | + |
| 129 | +- [CUDA编程概念】一、什么是bank conflict?](https://zhuanlan.zhihu.com/p/659142274) |
| 130 | +- [解决 bank conflict](https://github.com/PaddleJitLab/CUDATutorial/blob/develop/docs/09_optimize_reduce/02_bank_conflict/README.md) |
| 131 | +- [Bank Conflict free 的几种方式](https://zhuanlan.zhihu.com/p/722286440) |
| 132 | +- [Using Shared Memory in CUDA C/C++](https://developer.nvidia.com/blog/using-shared-memory-cuda-cc/) |
| 133 | +- [CUDA(三):通用矩阵乘法:从入门到熟练](https://zhuanlan.zhihu.com/p/657632577) |
| 134 | + |
14 | 135 | ## 测试
|
15 | 136 |
|
16 | 137 | ```bash
|
17 | 138 | # 只测试Ada架构 不指定默认编译所有架构 耗时较长: Volta, Ampere, Ada, Hopper, ...
|
18 | 139 | export TORCH_CUDA_ARCH_LIST=Ada
|
19 | 140 | python3 sgemm.py
|
20 | 141 | ```
|
21 |
| - |
22 | 142 | 输出:
|
23 | 143 |
|
24 | 144 | ```bash
|
|
0 commit comments