寫在前面:
- NVIDIA CUDA初級教程筆記 p11~p12
- 連接:https://www.bilibili.com/video/BV1kx411m7Fk?p=12
有效的數據並行算法 + 針對GPU架構特性的優化 = 最優性能
文章目錄
1.Parallel Reduction 並行規約
這個過程類似籃球錦標賽的淘汰過程:n個元素進行log(n)個回合,如何在CUDA上實現?
//累加存在shared memory內的元素
__shared__ float partialSum[element_num];
unsigned int t = threadIdx.x;
//步長:1,2,4...
for (unsigned int stride = 1; stride < blockDim.x; stride*=2)
{
__syncthreads();//保證每一步做完之後再進行下一步
//在同一塊shared memory裏面進行累加
//當步長增加時,多餘的線程在幹什麼?沒事幹
if (t%(2*stride)==0)
{
partialSum[t] += partialSum[t + stride];
}
}
如果我們改進這個過程會怎麼樣?
每一輪所需要的線程數依然是減半的,但是線程所處的位置不同
第二種可以將提前完成的線程的硬件資源釋放,用來做其他的事情。
元素求和源代碼
#include <stdio.h>
#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <stdlib.h>
/*
*處理有N個元素的並行規約
*/
# define N 2048
# define threadsPerBlock 512
# define blocksPerGrid (N+threadsPerBlock-1)/threadsPerBlock//4
__global__ void ReductioinSum_0(float* d_a, float* d_partial_sum)
{
//申請共享內存, 存在於每個block中
__shared__ float partialSum[threadsPerBlock];
//確定索引
unsigned int i = threadIdx.x+blockIdx.x*blockDim.x;
unsigned int tid = threadIdx.x;
//傳global mem數據到shared memory
partialSum[tid] = d_a[i];
//在共享存儲器中進行規約
//步長:1,2,4...
for (unsigned int stride = 1; stride < blockDim.x; stride*=2)
{
__syncthreads();//保證每一步做完之後再進行下一步
//在同一塊shared memory裏面進行累加
//當步長增加時,多餘的線程在幹什麼?沒事幹
if (tid%(2*stride)==0)
{
partialSum[tid] += partialSum[tid + stride];
}
}
//將當前block的計算結果寫回輸出數組
if (tid == 0)
d_partial_sum[blockIdx.x] = partialSum[0];
}
__global__ void ReductioinSum_01(float* d_a, float* d_partial_sum)
{
//申請共享內存, 存在於每個block中
__shared__ float partialSum[threadsPerBlock];
//確定索引
unsigned int i = threadIdx.x + blockIdx.x*blockDim.x;
unsigned int tid = threadIdx.x;
//傳global mem數據到shared memory
partialSum[tid] = d_a[i];
//在共享存儲器中進行規約
//步長:4.2.1.
for (unsigned int stride = blockDim.x / 2; stride > 0; stride /= 2)
{
__syncthreads();
if (tid<stride)
{
partialSum[tid] += partialSum[tid + stride];
}
}
//將當前block的計算結果寫回輸出數組
if (tid == 0)
d_partial_sum[blockIdx.x] = partialSum[0];
}
int main(void)
{
float a[N],partial_sum[blocksPerGrid];
//初始化示例數據
for (int i = 0; i<N; i++)
{
a[i] = i;
}
int size = N*sizeof(float);
//分配顯存空間
float *d_a,*d_partial_sum;
cudaMalloc((void**)&d_a, size);
cudaMemcpy(d_a, a, size, cudaMemcpyHostToDevice);
cudaMalloc((void**)&d_partial_sum, blocksPerGrid*sizeof(float));
ReductioinSum_01 <<<blocksPerGrid, threadsPerBlock >> >(d_a, d_partial_sum);
cudaMemcpy(partial_sum, d_partial_sum,blocksPerGrid*sizeof(float), cudaMemcpyDeviceToHost);
float sum = 0;
for (int i = 0; i < blocksPerGrid; i++) {
sum += partial_sum[i];
}
printf("all sum = %.2f", sum);
cudaFree(d_a);
cudaFree(d_partial_sum);
return 0;
}
2. Warp分割
Warp分割:塊內線程如何劃分wrap
通曉warp分割有助於:減少分支發散,讓warp儘早完工
- Block被劃分爲以32爲單位的線程組,叫做warp
- warp是最基本的調度單元
- Warp裏的線程一直執行相同的指令(SIMT)
- 每個線程只能執行自己的代碼路徑
- Fermi SM有2個warp調度器(Tesla has 1)
- warp裏設備切換沒有時間代價
- 許多warps在一起可以隱藏訪存延時
Warp分割的原則是:threadIdx連續增加的一組
以行爲主元的情況:
warp分支分散:會降低性能
例如:給定warpSize=32,以下代碼是否有哪個warp存在分支發散
if(threadIdx.x>15)
{
// 存在 ×
}
if(threadIdx.x>warpSize-1)
{
// 不存在 √
}
第二種更好
在第一輪,右邊的warp2和warp3可以騰出來做其他的事情
在第2輪,右邊的wap1 warp2和warp3可以騰出來做其他的事情
3.Memory Coalesing 訪存合併
上文了解了整個線程的調度和warp的切分後,我們來關注存儲優化問題。
CPU-GPU 數據傳輸最小化
- Host <- - > device數據傳輸帶寬遠低於global memory
- 減少傳輸
- 中間數據直接在GPU分配,操作,釋放
- 有時更適合在GPU進行重複運算
- 如果沒有減少數據傳輸的話,將CPU代碼一直到GPU可能無法提升性能
- 組團傳輸
- 大塊傳輸好於小塊:10微妙延遲,8GB/s => 如果數據小於80KB,性能將受延遲支配
- 內存傳輸與計算時間重疊
- 雙緩存
Coalesing合併
Global memory 延時:400 ~ 800 cycles
最重要的影響因子!
在Fermi,global memory默認緩存於 一級緩存L1
通過給nvcc指令設置參數 ”-Xptxas -dlcm=cg“可以繞過一級緩存L1:只緩存於二級緩存L2
如果緩存:warp的讀寫請求落到L1 cache line,只需一次傳輸
#transaction = #L1 line accessed
如果沒有緩存:有一些合併原則
但是傳輸大小可以減至32字節塊
Memory Coalescing 合併訪存
“相鄰的人搬相鄰的磚哈哈哈哈”
合併舉例
- 小型kernel拷貝數據時的有效帶寬
- 偏移和步長對性能的影響
- 2款GPUs
- GTX 280:compute capability 1.3;峯值帶寬141GB/2
- FX 5600:compute capability 1.0;峯值帶寬77GB/s
偏移量的影響
步長的影響
Shared memory
- 比global memory快上百倍
- 可以通過緩存數據減少global memory訪存次數
- 線程可以同通過shared memory協作
- 用來避免不滿足合併條件的訪存
- 讀入shared memory重排順序,從而支持合併尋址
4.Bank 衝突
按行讀,按列寫,讀寫總有一個是不合並的
讀和寫都實現合併訪存
Texture 紋理
總結
如果遵行一些簡單的原則,GPU硬件在數據可並行計算問題上,可以達到很好的性能:
- 有效利用並行性
- 儘可能合併內存訪問
- 利用shared memory
- 開發其他存儲空間
- Texture
- Constant
- 減少bank衝突
5.SM資源動態分割
Performance Cliff: 增加資源用量後導致並行性急劇下降。例如,增加寄存器數量,除非爲了隱藏global memory訪存延遲
kernel 啓動參數配置介紹
Grid Size試探法
Occupancy 佔用率
6.數據預讀
用來隱藏訪存延時的一個小手段
float m = Md[i];//read global memory
float f = a*b+c*d;//執行指令,不依賴於讀內存的操作
float f2 = m*f;//在上一行執行足夠多的warp隱藏訪存延時以後,再使用global memory
在上圖中
load next tile into register //爲循環的下一次迭代預讀
accumulate dot product //這些指令被足夠多的線程執行,從而隱藏了預讀內存產生的延時
7.指令混合
指令吞吐量優化
8.循環展開
缺點:可擴展性差,blocksize變化會帶來影響。