Cuda 學習 - Parallel Reduction(下) === * 本文接續上一篇 [Cuda 學習 - Parallel Reduction(上)](https://hackmd.io/lfQVUDnZSZ-j5hbRLENTOA) 做後續的演練 * 在上一篇已完成了 Parallel reduction 的介紹及基本的程式演練但在 Sequential Addressing 上面雖然已完成加速但造成了一半以上的 threads 呈現 ideal 狀態, 這是非常浪費資源的事情 # First Add During Load * 由於後半的 thread 基本上都為使用到那和不一開始就先往前加到前半呢 * 可以看到直接將 i 的部份前半的 block offset 乘上兩倍並放到 shared memory 上 * 迴圈的 offset 也改由 blockDim.x 的一半開始, 每次新的迭代就除以 2, 並針對小於 offset 的 tid 進行加總 ```cpp= __global__ void reduce3(int *g_idata, int *g_odata) { extern __shared__ int sdata[]; // each thread loads one element from global to shared mem unsigned int tid = threadIdx.x; unsigned int i = blockIdx.x*(blockDim.x*2) + threadIdx.x; sdata[tid] = g_idata[i] + g_idata[i+blockDim.x]; __syncthreads(); for (unsigned int s=blockDim.x/2; s>0; s>>=1) { if (tid < s) { sdata[tid] += sdata[tid + s]; } __syncthreads(); } // write result for this block to global mem if (tid == 0) g_odata[blockIdx.x] = sdata[0]; } ``` * 然而實際計算還離原有 Bandwidth 有較大差距, 原因在於 address 的運算及迴圈運算 # Unroll the Last Warp * 在運算的最後我們只會剩下一個 Warp 去使用, 而當 thread <= 32 可不用在使用 __synthreads 及其於同步控制 * Warp 跟 Warp 之間才需要做同步, 單一個 Warp 內的 thread 本身會執行同時執行同一個指令 * 在 Unroll the last warp 時需要使用 volatile 來做每次資料正確的讀取 ```cpp= __device__ void warpReduce(volatile int* sdata, int tid) { sdata[tid] += sdata[tid + 32]; sdata[tid] += sdata[tid + 16]; sdata[tid] += sdata[tid + 8]; sdata[tid] += sdata[tid + 4]; sdata[tid] += sdata[tid + 2]; sdata[tid] += sdata[tid + 1]; } __global__ void reduce4(int *g_idata, int *g_odata) { extern __shared__ int sdata[]; // each thread loads one element from global to shared mem unsigned int tid = threadIdx.x; unsigned int i = blockIdx.x*(blockDim.x*2) + threadIdx.x; sdata[tid] = g_idata[i] + g_idata[i+blockDim.x]; __syncthreads(); for (unsigned int s=blockDim.x/2; s>32; s>>=1) { if (tid < s) { sdata[tid] += sdata[tid + s]; } __syncthreads(); } // write result for this block to global mem if (tid < 32) { warpReduce(sdata, tid); } if (tid == 0) g_odata[blockIdx.x] = sdata[0]; } ``` # Completely Unrolled * 剛剛我們只展開了最後一個 Warp , 現在來將 for 迴圈也做展開 ```cpp= __global__ void reduce5(int *g_idata, int *g_odata) { extern __shared__ int sdata[]; // each thread loads one element from global to shared mem unsigned int tid = threadIdx.x; unsigned int i = blockIdx.x*(blockDim.x*2) + threadIdx.x; sdata[tid] = g_idata[i] + g_idata[i+blockDim.x]; __syncthreads(); if(blockDim.x>=512 && tid<256) { sdata[tid] += sdata[tid+256]; } __syncthreads(); if(blockDim.x>=256 && tid<128) { sdata[tid] += sdata[tid+128]; } __syncthreads(); if(blockDim.x>=128 && tid<64) { sdata[tid] += sdata[tid+64]; } __syncthreads(); // write result for this block to global mem if (tid < 32) { warpReduce(sdata, tid); } if (tid == 0) g_odata[blockIdx.x] = sdata[0]; } ``` # 計算結果 * 硬體環境 * 筆電 : Dell G15 * GPU : RTX3050 Laptop 4G * CPU : 12th Gen Intel(R) Core(TM) i5-12500H, 16 cores * Memory : 16GB * 計算任務 * 2^22 的 Array 內的元素和 * block : 128 * grid : (2^22/128)/2 * Grid 的部份從 First Add During Load 開始因為將後半的資料往前加, 所以 grid 本身也只需要一半即可完成 | Method | Time (ms)| Bandwidth (GB/s) | Grid | block | ------------------------------------ | -------- | -----------------|------------|------ | Interleaved Addressing | 0.5519 | 56.6171 |2^22/128 |128 | Interleaved Addressing (Improved) | 0.493769 | 63.2887 |2^22/128 |128 | Sequential Addressing | 0.473715 | 65.9679 |2^22/128 |128 | First Add During Load | 0.341927 | 91.3938 |(2^22/128)/2|128 | Unroll the Last Warp | 0.307297 | 101.693 |(2^22/128)/2|128 | Completely Unrolled | 0.263343 | 118.667 |(2^22/128)/2|128 * ![image](https://hackmd.io/_uploads/HkuGbhj0R.png) * 從 Interleaved Addressing 到 Completely Unrolled 已加速 2.09X # 結論 * 我們完成了 Parallel Reduction 的許多小 trick, 例如善用 Warp 的機制以及盡可能的使用每個 thread 進而不造成資源的浪費, 而在 IA proved 及 SA 須再確認 差異不大的原因 source : https://developer.download.nvidia.com/assets/cuda/files/reduction.pdf https://blog.csdn.net/langb2014/article/details/51348523 * github : https://github.com/GU-Lin/Cuda_Practice/tree/main/5_Parallel_Reduction * ###### tags : `Cuda`