本文來源公眾號“DeepDriving”,僅用于學術分享,侵權刪,干貨滿滿。
原文鏈接:CUDA編程-02: 初識CUDA編程
上一篇文章DeepDriving | CUDA編程-01: 搭建CUDA編程環境-CSDN博客介紹了如何搭建CUDA編程環境,從這篇文章開始正式開始介紹如何使用CUDA進行編程。
1 異構計算架構
如下圖所示,一個典型的異構計算架構節點由一個多核CPU
和一個或多個GPU
組成,每個CPU
和GPU
都是獨立的設備,它們之間通過PCIe
總線連接。GPU
作為CPU
的協處理器用于執行一些并行計算任務。CPU
適合用于做一些邏輯控制任務,而GPU
則適合作為CPU
的協處理器用于做一些大計算量的數據并行計算任務。
heterogeneous_architecture
一般我們稱CPU
為host
,稱GPU
為device
,相應地,一個異構計算的應用程序代碼也被分為兩部分:運行在CPU
上的程序被稱為host
代碼,運行在GPU
上的程序被稱為device
代碼。
heterogeneous_architecture
2 一個Hello World
在我們學習一種新的編程語言的時候,一般都是先從打印一句"Hello World"
開始,今天開始學習CUDA
編程,按照國際慣例,也從打印"Hello World"
開始。
首先新建一個CUDA C
源文件hello_world.cu
,然后輸入下面的內容:
#include <stdio.h>int main(void)
{printf("Hello World from CPU\n");return 0;
}
用nvcc
進行編譯生成可執行文件:
nvcc?hello_world.cu?-o?hello_world
運行可執行文件hello_world
,沒有意外的話就能在終端看到打印出的"Hello World from CPU"
了。這是在CPU
上運行代碼打印了這句話,那么怎么在GPU
上打印呢?
要在GPU
上運行程序,我們需要寫一個能在GPU
上執行的函數,然后在CPU
上調用這個函數。來看一個例子:
#include <stdio.h>__global__ void HelloFromGPU(void)
{printf("Hello from GPU\n");
}int main(void)
{printf("Hello from CPU\n");HelloFromGPU<<<1, 5>>>();cudaDeviceReset();return 0;
}
在上面的這段代碼里,我添加了一個用__global__
函數類型限定符修飾的函數HelloFromGPU
,然后在main
函數中去調用它。在CUDA
中,有以下3種函數類型限定符:
_global_
被__global__
函數類型限定符修飾的函數被稱為內核函數,該函數在host
上被調用,在device
上執行,只能返回void
類型,不能作為類的成員函數。調用__global__
修飾的函數是異步的,也就是說它未執行完就會返回。
_device_
被__device__
函數類型限定符修飾的函數只能在device
上被調用,在device
上執行,用于在device
代碼中內部調用。
_host_
被__host__
函數類型限定符修飾的函數只能在host
上被調用,在host
上執行,也就是host
上的函數,__host__
函數類型限定符可以省略。
與調用一般CPU
函數不同的是,調用HelloFromGPU
函數的時候會在后面寫上<<< >>>
,意思這是從host
端到device
端的內核函數調用,里面的參數是執行配置,用來說明使用多少線程來執行內核函數。一個內核函數是通過一組線程來執行的,所有線程執行的同樣的代碼,我這里設置的是用了5個線程來執行。程序編譯成功后運行得到如下結果:
Hello from CPU
Hello from GPU
Hello from GPU
Hello from GPU
Hello from GPU
Hello from GPU
可以看到,內核函數里的打印語句被執行了5次。
3 用CUDA實現數組相加
一個典型的CUDA
程序結構一般由以下5個步驟組成:
-
分配
GPU
內存; -
從
CPU
內存中拷貝數據到GPU
內存中; -
調用
CUDA
內核函數執行程序指定的計算任務; -
從
GPU
內存中把數據拷貝回CPU
內存中; -
釋放
GPU
內存;
下面以一個數組相加的例子來展示以上過程,數組相加的計算過程比較簡單:數組a和數組b中對應下標的元素相加然后存入數組c中。
vector_add
首先來看一下在CPU
上實現數組相加的代碼:
#include?<iostream>void?VectorAddCPU(const?float?*const?a,?const?float?*const?b,?float?*const?c,const?int?n)?{for?(int?i?=?0;?i?<?n;?++i)?{c[i]?=?a[i]?+?b[i];}
}int?main(void)?{//?alloc?memory?for?hostconst?size_t?size?=?1024;float?*ha?=?new?float[size]();float?*hb?=?new?float[size]();float?*hc?=?new?float[size]();for?(int?i?=?0;?i?<?size;?++i)?{ha[i]?=?i;hb[i]?=?size?-?i;}VectorAddCPU(ha,?hb,?hc,?size);delete[]?ha;delete[]?hb;delete[]?hc;return?0;
}
函數VectorAddCPU
用了一個for
循環在CPU
上實現數組相加的過程。如果要用GPU
來實現該過程,則調用CUDA
的API
按照前面說的5個步驟編寫代碼:
#include?<cuda_runtime.h>
#include?<iostream>__global__?void?VectorAddGPU(const?float?*const?a,?const?float?*const?b,float?*const?c,?const?int?n)?{int?i?=?blockDim.x?*?blockIdx.x?+?threadIdx.x;if?(i?<?n)?{c[i]?=?a[i]?+?b[i];}
}int?main(void)?{//?分配CPU內存const?size_t?size?=?1024;float?*ha?=?new?float[size]();float?*hb?=?new?float[size]();float?*hc?=?new?float[size]();for?(int?i?=?0;?i?<?size;?++i)?{ha[i]?=?i;hb[i]?=?size?-?i;}//?分配GPU內存float?*da?=?nullptr;float?*db?=?nullptr;float?*dc?=?nullptr;cudaMalloc((void?**)&da,?size);cudaMalloc((void?**)&db,?size);cudaMalloc((void?**)&dc,?size);//?把數據從CPU拷貝到GPUcudaMemcpy(da,?ha,?size,?cudaMemcpyHostToDevice);cudaMemcpy(db,?hb,?size,?cudaMemcpyHostToDevice);cudaMemcpy(dc,?hc,?size,?cudaMemcpyHostToDevice);const?int?thread_per_block?=?256;const?int?block_per_grid?=?(size?+?thread_per_block?-?1)?/?thread_per_block;//?調用核函數VectorAddGPU<<<block_per_grid,?thread_per_block>>>(da,?db,?dc,?size);//?把數據從GPU拷貝回CPUcudaMemcpy(hc,?dc,?size,?cudaMemcpyDeviceToHost);//?釋放GPU內存cudaFree(da);cudaFree(db);cudaFree(dc);//?釋放CPU內存delete[]?ha;delete[]?hb;delete[]?hc;return?0;
}
在這段代碼里,用到了幾個CUDA
的內存管理函數:
cudaMalloc
函數原型:
cudaError_t cudaMalloc(void** devPtr, size_t size)
該函數用于在GPU
上分配指定大小的內存空間,類似于標準C語言中的malloc
函數。
cudaMemcpy
函數原型:
cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)
該函數用于內存拷貝,類似于標準C語言中的memcpy
函數。數據拷貝的流向由參數kind
指定,分為以下4種方式:
-
cudaMemcpyHostToHost
-
cudaMemcpyHostToDevice
-
cudaMemcpyDeviceToHost
-
cudaMemcpyDeviceToDevice
從它們的字面意思就能知道,如果是從host
拷貝數據到device
,那么kind
參數應該設置為cudaMemcpyHostToDevice
;如果是從device
拷貝數據到host
,那么kind
參數應該設置為cudaMemcpyDeviceToHost
。上面的代碼體現了這一點。
cudaFree
udaError_t cudaFree(void* devPtr)
該函數用于釋放已分配的GPU
內存空間,類似于標準C語言中的free
函數。
在上面的代碼中,內核函數VectorAddGPU
用于實現數組相加,與前面的代碼中VectorAddCPU
函數不同的是,VectorAddGPU
函數里面并沒有用for
循環,因為在GPU
中是將數據進行并行化劃分,然后通過線程組去實現計算過程的,線程組中的每個線程都是執行c[i] = a[i] + b[i]
這個計算過程。在這個程序里,數組的長度為1024
,我設置了每個線程組中的線程數量為256
,總共用了4個線程組去進行計算。
關于GPU
中線程和線程組的相關知識,我將在下一篇文章中進行闡述。
4 參考資料
-
《
Professional CUDA C Programming
》 -
《
CUDA C Programming_Guide
》
THE END !
文章結束,感謝閱讀。您的點贊,收藏,評論是我繼續更新的動力。大家有推薦的公眾號可以評論區留言,共同學習,一起進步。