CPU、GPU、CUDA,CuDNN 介紹

原文:https://blog.csdn.net/fangjin_kl/article/details/53906874

一、CPU和GPU的區別

  • CPU (Central Processing Unit) 即中央處理器
  • GPU (Graphics Processing Unit) 即圖形處理器
  • GPGPU全稱General Purpose GPU,即通用計算圖形處理器。其中第一個“GP”通用目的(GeneralPurpose)而第二個“GP”則表示圖形處理(GraphicProcess)

CPU雖然有多核,但總數沒有超過兩位數,每個核都有足夠大的緩存和足夠多的數字和邏輯運算單元,並輔助有很多加速分支判斷甚至更復雜的邏輯判斷的硬件;

GPU的核數遠超CPU,被稱爲衆核(NVIDIA Fermi有512個核)。每個核擁有的緩存大小相對小,數字邏輯運算單元也少而簡單(GPU初始時在浮點計算上一直弱於CPU)。

從結果上導致CPU擅長處理具有複雜計算步驟和複雜數據依賴的計算任務,如分佈式計算,數據壓縮,人工智能,物理模擬,以及其他很多很多計算任務等。

GPU由於歷史原因,是爲了視頻遊戲而產生的(至今其主要驅動力還是不斷增長的視頻遊戲市場),在三維遊戲中常常出現的一類操作是對海量數據進行相同的操作,如:對每一個頂點進行同樣的座標變換,對每一個頂點按照同樣的光照模型計算顏色值。GPU的衆核架構非常適合把同樣的指令流並行發送到衆核上,採用不同的輸入數據執行

當程序員爲CPU編寫程序時,他們傾向於利用複雜的邏輯結構優化算法從而減少計算任務的運行時間,即Latency。
當程序員爲GPU編寫程序時,則利用其處理海量數據的優勢,通過提高總的數據吞吐量(Throughput)來掩蓋Lantency

這裏寫圖片描述

其中綠色的是計算單元,橙紅色的是存儲單元,橙黃色的是控制單元。

GPU採用了數量衆多的計算單元和超長的流水線,但只有非常簡單的控制邏輯並省去了Cache。而CPU不僅被Cache佔據了大量空間,而且還有有複雜的控制邏輯和諸多優化電路,相比之下計算能力只是CPU很小的一部分

二、CUDA

CUDA(Compute Unified Device Architecture),是英偉達公司推出的一種基於新的並行編程模型和指令集架構的通用計算架構,它能利用英偉達GPU的並行計算引擎,比CPU更高效的解決許多複雜計算任務。

使用CUDA的好處就是透明。根據摩爾定律GPU的晶體管數量不斷增多,硬件結構必然是不斷的在發展變化,沒有必要每次都爲不同的硬件結構重新編碼,而CUDA就是提供了一種可擴展的編程模型,使得已經寫好的CUDA代碼可以在任意數量核心的GPU上運行。如下圖所示,只有運行時,系統才知道物理處理器的數量。
這裏寫圖片描述

三、CuDNN

NVIDIA cuDNN是用於深度神經網絡的GPU加速庫。它強調性能、易用性和低內存開銷。NVIDIA cuDNN可以集成到更高級別的機器學習框架中,如加州大學伯克利分校的流行CAFFE軟件。簡單的,插入式設計可以讓開發人員專注於設計和實現神經網絡模型,而不是調整性能,同時還可以在GPU上實現高性能現代並行計算。

cuDNN 用戶手冊(英文)

CuDNN支持的算法

  1. 卷積操作、相關操作的前向和後向過程。
  2. pooling的前向後向過程
  3. softmax的前向後向過程
  4. 激活函數的前向後向過程
    • ReLU
    • sigmoid
    • TANH
  5. Tensor轉換函數,其中一個Tensor就是一個四維的向量。

這裏寫圖片描述
Baseline Caffe與用NVIDIA Titan Z 加速cuDNN的Caffe做比較

四、CUDA編程

參考自 一篇不錯的CUDA入門博客

開發人員可以通過調用CUDA的API,來進行並行編程,達到高性能計算目的。NVIDIA公司爲了吸引更多的開發人員,對CUDA進行了編程語言擴展,如CUDA C/C++,CUDA Fortran語言。注意CUDA C/C++可以看作一個新的編程語言,因爲NVIDIA配置了相應的編譯器nvcc,CUDA Fortran一樣。

如果粗暴的認爲C語言工作的對象是CPU和內存條(接下來,稱爲主機內存),那麼CUDA C工作的的對象就是GPU及GPU上的內存(接下來,稱爲設備內存),且充分利用了GPU多核的優勢及降低了並行編程的難度。一般通過C語言把數據從外界讀入,再分配數據,給CUDA C,以便在GPU上計算,然後再把計算結果返回給C語言,以便進一步工作,如進一步處理及顯示,或重複此過程。

主要概念與名稱

  1. 主機
    將CPU及系統的內存(內存條)稱爲主機。
  2. 設備
    將GPU及GPU本身的顯示內存稱爲設備。
  3. 線程(Thread)
    一般通過GPU的一個核進行處理。(可以表示成一維,二維,三維,具體下面再細說)。
  4. 線程塊(Block)
    1. 由多個線程組成(可以表示成一維,二維,三維,具體下面再細說)。
    2. 各block是並行執行的,block間無法通信,也沒有執行順序。
    3. 注意線程塊的數量限制爲不超過65535(硬件限制)。
  5. 線程格(Grid)
    由多個線程塊組成(可以表示成一維,二維,三維,具體下面再細說)。
  6. 線程束
    在CUDA架構中,線程束是指一個包含32個線程的集合,這個線程集合被“編織在一起”並且“步調一致”的形式執行。在程序中的每一行,線程束中的每個線程都將在不同數據上執行相同的命令。
  7. 核函數(Kernel)
    1. 在GPU上執行的函數通常稱爲核函數。
    2. 一般通過標識符__global__修飾,調用通過<<<參數1,參數2>>>,用於說明內核函數中的線程數量,以及線程是如何組織的。
    3. 以線程格(Grid)的形式組織,每個線程格由若干個線程塊(block)組成,而每個線程塊又由若干個線程(thread)組成。
    4. 是以block爲單位執行的。
    5. 只能在主機端代碼中調用。
    6. 調用時必須聲明內核函數的執行參數。
    7. 在編程時,必須先爲kernel函數中用到的數組或變量分配好足夠的空間,再調用kernel函數,否則在GPU計算時會發生錯誤,例如越界或報錯,甚至導致藍屏和死機。

這裏寫圖片描述

/*
 * @file_name HelloWorld.cu  後綴名稱.cu
 */

#include <stdio.h>
#include <cuda_runtime.h>  //頭文件

//核函數聲明,前面的關鍵字__global__
__global__ void kernel( void ) {
}

int main( void ) {
    //核函數的調用,注意<<<1,1>>>,第一個1,代表線程格里只有一個線程塊;第二個1,代表一個線程塊裏只有一個線程。
    kernel<<<1,1>>>();
    printf( "Hello, World!\n" );
    return 0;
}

dim3結構類型

  1. dim3是基於 uint3 定義的矢量類型,相當亍由3個unsigned int型組成的結構體。uint3類型有三個數據成員unsigned int x; unsigned int y; unsigned int z;
  2. 可使用一維、二維或三維的索引來標識線程,構成一維、二維或三維線程塊。
  3. dim3結構類型變量用在覈函數調用的<<<,>>>中。
  4. 相關的幾個內置變量
    • threadIdx,顧名思義獲取線程 thread 的ID索引;如果線程是一維的那麼就取 threadIdx.x,二維的還可以多取到一個值threadIdx.y,以此類推到三維threadIdx.z。
    • blockIdx,線程塊的ID索引;同樣有blockIdx.x,blockIdx.y,blockIdx.z。
    • blockDim,線程塊的維度,同樣有blockDim.x,blockDim.y,blockDim.z。
    • gridDim,線程格的維度,同樣有gridDim.x,gridDim.y,gridDim.z。
  5. 對於一維的block,線程的threadID=threadIdx.x。
  6. 對於大小爲(blockDim.x, blockDim.y)的 二維 block,線程的threadID=threadIdx.x+threadIdx.y*blockDim.x。
  7. 對於大小爲(blockDim.x, blockDim.y, blockDim.z)的 三維 block,線程的threadID=threadIdx.x+threadIdx.y*blockDim.x+threadIdx.z*blockDim.x*blockDim.y。
  8. 對於計算線程索引偏移增量爲已啓動線程的總數。如stride = blockDim.x * gridDim.x; threadId += stride。9.

函數修飾符

  1. __global__,表明被修飾的函數在設備上執行,但在主機上調用。
  2. __device__,表明被修飾的函數在設備上執行,但只能在其他device函數或者global函數中調用。

常用的GPU內存函數

cudaMalloc()

  1. 函數原型: cudaError_t cudaMalloc (void **devPtr, size_t size)
  2. 函數用處:與C語言中的malloc函數一樣,只是此函數在GPU的內存你分配內存。
  3. 注意事項:
    • 可以將cudaMalloc()分配的指針傳遞給在設備上執行的函數;
    • 可以在設備代碼中使用cudaMalloc()分配的指針進行設備內存讀寫操作;
    • 可以將cudaMalloc()分配的指針傳遞給在主機上執行的函數;
    • 不可以在主機代碼中使用cudaMalloc()分配的指針進行主機內存讀寫操作(即不能進行解引用)。

cudaMemcpy()

  1. 函數原型:cudaError_t cudaMemcpy (void *dst, const void *src, size_t count, cudaMemcpyKind kind)
  2. 函數作用:與c語言中的memcpy函數一樣,只是此函數可以在主機內存和GPU內存之間互相拷貝數據。
  3. 函數參數:cudaMemcpyKind kind表示數據拷貝方向,如果kind賦值爲cudaMemcpyDeviceToHost表示數據從設備內存拷貝到主機內存。
  4. 與C中的memcpy()一樣,以同步方式執行,即當函數返回時,複製操作就已經完成了,並且在輸出緩衝區中包含了複製進去的內容。
  5. 相應的有個異步方式執行的函數cudaMemcpyAsync(),這個函數詳解請看下面的流一節有關內容。

cudaFree()

  1. 函數原型:cudaError_t cudaFree ( void* devPtr )
  2. 函數作用:與c語言中的free()函數一樣,只是此函數釋放的是cudaMalloc()分配的內存。

下面實例用於解釋上面三個函數

#include <stdio.h>
#include <cuda_runtime.h>
__global__ void add( int a, int b, int *c ) {
    *c = a + b;
}
int main( void ) {
    int c;
    int *dev_c;
    //cudaMalloc()
    cudaMalloc( (void**)&dev_c, sizeof(int) );
    //核函數執行
    add<<<1,1>>>( 2, 7, dev_c );   
    //cudaMemcpy()
    cudaMemcpy( &c, dev_c, sizeof(int),cudaMemcpyDeviceToHost ) ;
    printf( "2 + 7 = %d\n", c );
    //cudaFree()
    cudaFree( dev_c );

    return 0;
}

GPU內存分類

全局內存

通俗意義上的設備內存。

共享內存

  1. 位置:設備內存。
  2. 形式:關鍵字shared添加到變量聲明中。如shared float cache[10]。
  3. 目的:對於GPU上啓動的每個線程塊,CUDA C編譯器都將創建該共享變量的一個副本。線程塊中的每個線程都共享這塊內存,但線程卻無法看到也不能修改其他線程塊的變量副本。這樣使得一個線程塊中的多個線程能夠在計算上通信和協作。

常量內存

  1. 位置:設備內存
  2. 形式:關鍵字constant添加到變量聲明中。如constant float s[10];。
  3. 目的:爲了提升性能。常量內存採取了不同於標準全局內存的處理方式。在某些情況下,用常量內存替換全局內存能有效地減少內存帶寬。
  4. 特點:常量內存用於保存在覈函數執行期間不會發生變化的數據。變量的訪問限制爲只讀。NVIDIA硬件提供了64KB的常量內存。不再需要cudaMalloc()或者cudaFree(),而是在編譯時,靜態地分配空間。
  5. 要求:當我們需要拷貝數據到常量內存中應該使用cudaMemcpyToSymbol(),而cudaMemcpy()會複製到全局內存。
  6. 性能提升的原因:
    • 對常量內存的單次讀操作可以廣播到其他的“鄰近”線程。這將節約15次讀取操作。(爲什麼是15,因爲“鄰近”指半個線程束,一個線程束包含32個線程的集合。)
    • 常量內存的數據將緩存起來,因此對相同地址的連續讀操作將不會產生額外的內存通信量。

紋理內存

  1. 位置:設備內存
  2. 目的:能夠減少對內存的請求並提供高效的內存帶寬。是專門爲那些在內存訪問模式中存在大量空間局部性的圖形應用程序設計,意味着一個線程讀取的位置可能與鄰近線程讀取的位置“非常接近”。如下圖:
  3. 紋理變量(引用)必須聲明爲文件作用域內的全局變量。
  4. 形式:分爲一維紋理內存 和 二維紋理內存。
    • 一維紋理內存
      • texture<類型>類型聲明,如texture<float> texIn
      • 通過cudaBindTexture()綁定到紋理內存中。
      • 通過tex1Dfetch()來讀取紋理內存中的數據。
      • 通過cudaUnbindTexture()取消綁定紋理內存。
    • 二維紋理內存
      • texture<類型,數字>類型聲明,如texture<float,2> texIn
      • 通過cudaBindTexture2D()綁定到紋理內存中。
      • 通過tex2D()來讀取紋理內存中的數據。
      • 通過cudaUnbindTexture()取消綁定紋理內存。

這裏寫圖片描述

固定內存

  1. 位置:主機內存。
  2. 概念:也稱爲頁鎖定內存或者不可分頁內存,操作系統將不會對這塊內存分頁並交換到磁盤上,從而確保了該內存始終駐留在物理內存中。因此操作系統能夠安全地使某個應用程序訪問該內存的物理地址,因爲這塊內存將不會破壞或者重新定位。
  3. 目的:提高訪問速度。由於GPU知道主機內存的物理地址,因此可以通過“直接內存訪問DMA(Direct Memory Access)技術來在GPU和主機之間複製數據。由於DMA在執行復制時無需CPU介入。因此DMA複製過程中使用固定內存是非常重要的。
  4. 缺點:使用固定內存,將失去虛擬內存的所有功能;系統將更快的耗盡內存。
  5. 建議:對cudaMemcpy()函數調用中的源內存或者目標內存,才使用固定內存,並且在不再需要使用它們時立即釋放。
  6. 形式:通過cudaHostAlloc()函數來分配;通過cudaFreeHost()釋放。
  7. 只能以異步方式對固定內存進行復制操作。

原子性

  1. 概念:如果操作的執行過程不能分解爲更小的部分,我們將滿足這種條件限制的操作稱爲原子操作。
  2. 形式:函數調用,如atomicAdd(addr,y)將生成一個原子的操作序列,這個操作序列包括讀取地址addr處的值,將y增加到這個值,以及將結果保存回地址addr。

常用線程操作函數

  1. 同步方法__syncthreads(),這個函數的調用,將確保線程塊中的每個線程都執行完__syscthreads()前面的語句後,纔會執行下一條語句。

使用事件來測量性能

  1. 用途:爲了測量GPU在某個任務上花費的時間。CUDA中的事件本質上是一個GPU時間戳。由於事件是直接在GPU上實現的。因此不適用於對同時包含設備代碼和主機代碼的混合代碼設計。
  2. 形式:首先創建一個事件,然後記錄事件,再計算兩個事件之差,最後銷燬事件。如:
cudaEvent_t start, stop;
cudaEventCreate( &start );
cudaEventCreate( &stop );
cudaEventRecord( start, 0 );
//do something
cudaEventRecord( stop, 0 );
float   elapsedTime;
cudaEventElapsedTime( &elapsedTime,start, stop );
cudaEventDestroy( start );
cudaEventDestroy( stop );

  1. 扯一扯:併發重點在於一個極短時間段內運行多個不同的任務;並行重點在於同時運行一個任務。
  2. 任務並行性:是指並行執行兩個或多個不同的任務,而不是在大量數據上執行同一個任務。
  3. 概念:CUDA流表示一個GPU操作隊列,並且該隊列中的操作將以指定的順序執行。我們可以在流中添加一些操作,如核函數啓動,內存複製以及事件的啓動和結束等。這些操作的添加到流的順序也是它們的執行順序。可以將每個流視爲GPU上的一個任務,並且這些任務可以並行執行。
  4. 硬件前提:必須是支持設備重疊功能的GPU。支持設備重疊功能,即在執行一個核函數的同時,還能在設備與主機之間執行復制操作。
  5. 聲明與創建:聲明cudaStream_t stream;,創建cudaSteamCreate(&stream);。
  6. cudaMemcpyAsync():前面在cudaMemcpy()中提到過,這是一個以異步方式執行的函數。在調用cudaMemcpyAsync()時,只是放置一個請求,表示在流中執行一次內存複製操作,這個流是通過參數stream來指定的。當函數返回時,我們無法確保複製操作是否已經啓動,更無法保證它是否已經結束。我們能夠得到的保證是,複製操作肯定會當下一個被放入流中的操作之前執行。傳遞給此函數的主機內存指針必須是通過cudaHostAlloc()分配好的內存。(流中要求固定內存)
  7. 流同步:通過cudaStreamSynchronize()來協調。
  8. 流銷燬:在退出應用程序之前,需要銷燬對GPU操作進行排隊的流,調用cudaStreamDestroy()
  9. 針對多個流:
    • 記得對流進行同步操作。
    • 將操作放入流的隊列時,應採用寬度優先方式,而非深度優先的方式,換句話說,不是首先添加第0個流的所有操作,再依次添加後面的第1,2,…個流。而是交替進行添加,比如將a的複製操作添加到第0個流中,接着把a的複製操作添加到第1個流中,再繼續其他的類似交替添加的行爲。
    • 要牢牢記住操作放入流中的隊列中的順序影響到CUDA驅動程序調度這些操作和流以及執行的方式。

技巧

  1. 當線程塊的數量爲GPU中處理數量的2倍時,將達到最優性能。
  2. 核函數執行的第一個計算就是計算輸入數據的偏移。每個線程的起始偏移都是0到線程數量減1之間的某個值。然後,對偏移的增量爲已啓動線程的總數。

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