CUDA程序基本優化

寫在前面:

  • 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 衝突

### shared memory架構

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
按行讀,按列寫,讀寫總有一個是不合並的
在這裏插入圖片描述
在這裏插入圖片描述
讀和寫都實現合併訪存
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

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變化會帶來影響。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章