【CUDA】CUDA編譯

一、引言

1、GPU架構特點

CUDA(Compute Unified Device Architecture):是NVIDIA推出的用於自家GPU的並行計算框架。只有安裝這個框架才能夠進行復雜的並行計算。主流的深度學習框架也都是基於CUDA進行GPU並行加速的,幾乎無一例外。還有一個叫做cudnn,是針對深度卷積神經網絡的加速庫。開發人員可以使用C語言來爲CUDA架構編寫程序,C語言是應用最廣泛的一種高級編程語言。所編寫出的程序可以在支持CUDA的處理器上以超高性能運行。通過[1]對CUDA有個初步瞭解。

在談並行、串行計算時多次談到“多核”的概念,先從“核”的角度開始這個話題,首先CPU是專爲順序串行處理而優化的幾個核心組成;而GPU則由數以千計的更小、更高效的核心組成,這些核心專門爲同時處理多任務而設計,可高效地處理並行任務。也就是,CPU雖然每個核心自身能力極強,處理任務上非常強悍,無奈他核心少,在並行計算上表現不佳;反觀GPU,雖然他的每個核心的計算能力不算強,但他勝在核心非常多,可以同時處理多個計算任務,在並行計算的支持上做得很好。GPU和CPU的不同硬件特點決定了他們的應用場景,CPU是計算機的運算和控制的核心,GPU主要用作圖形圖像處理。圖像在計算機呈現的形式就是矩陣,我們對圖像的處理其實就是操作各種矩陣進行計算,而很多矩陣的運算其實可以做並行化,這使得圖像處理可以做得很快,因此GPU在圖形圖像領域也有了大展拳腳的機會。

現在再從數據處理的角度來對比CPU和GPU的特點。CPU需要很強的通用性來處理各種不同的數據類型,比如整型、浮點數等,同時它又必須擅長處理邏輯判斷所導致的大量分支跳轉和中斷處理,所以CPU其實就是一個能力很強的夥計,他能把很多事處理得妥妥當當,當然啦我們需要給他很多資源供他使用(各種硬件),這也導致了CPU不可能有太多核心(核心總數不超過16)。而GPU面對的則是類型高度統一的、相互無依賴的大規模數據和不需要被打斷的純淨的計算環境,GPU有非常多核心(費米架構就有512核),雖然其核心的能力遠沒有CPU的核心強,但是勝在多,在處理簡單計算任務時呈現出“人多力量大”的優勢,這就是並行計算的魅力。

整理一下兩者特點就是:

  • CPU:擅長流程控制和邏輯處理,不規則數據結構,不可預測存儲結構,單線程程序,分支密集型算法
  • GPU:擅長數據並行計算,規則數據結構,可預測存儲模式

現在的計算機體系架構中,要完成CUDA並行計算,單靠GPU一人之力是不能完成計算任務的,必須藉助CPU來協同配合完成一次高性能的並行計算任務。一般而言,並行部分在GPU上運行,串行部分在CPU運行,這就是異構計算。具體一點,異構計算的意思就是不同體系結構的處理器相互協作完成計算任務。CPU負責總體的程序流程,而GPU負責具體的計算任務,當GPU各個線程完成計算任務後,我們就將GPU那邊計算得到的結果拷貝到CPU端,完成一次計算任務。

2、CUDA線程模型

線程是程序執行的最基本單元,CUDA的並行計算就是通過成千上萬個線程的並行執行來實現的。

1. Thread:線程,並行的基本單位

2. Thread Block:線程塊,互相合作的線程組,線程塊有如下幾個特點:

  • 允許彼此同步
  • 可以通過共享內存快速交換數據
  • 以1維、2維或3維組織

3. Grid:一組線程塊

  • 以1維、2維組織
  • 共享全局內存

4. Kernel:在GPU上執行的核心程序,這個kernel函數是運行在某個Grid上的。

  • One kernel <——> One Grid

每一個block和每個thread都有自己的ID,我們通過相應的索引找到相應的線程和線程塊。

  • threadIdx,blockIdx
  • Block ID: 1D or 2D
  • Thread ID: 1D, 2D or 3D

kernel在device上執行時實際上是啓動很多線程,一個kernel所啓動的所有線程稱爲一個網格(grid),同一個網格上的線程共享相同的全局內存空間,grid是線程結構的第一層次,而網格又可以分爲很多線程塊(block),一個線程塊裏面包含很多線程,這是第二個層次。線程兩層組織結構如上圖所示,這是一個gird和block均爲2-dim的線程組織。grid和block都是定義爲dim3類型的變量,dim3可以看成是包含三個無符號整數(x,y,z)成員的結構體變量,在定義時,缺省值初始化爲1。因此grid和block可以靈活地定義爲1-dim,2-dim以及3-dim結構,kernel調用時也必須通過執行配置<<<grid, block>>>來指定kernel所使用的網格維度和線程塊維度。舉個例子,以上圖爲例,分析怎麼通過<<<grid,block>>>>這種標記方式索引到我們想要的那個線程。CUDA的這種<<<grid,block>>>其實就是一個多級索引的方法,第一級索引是(grid.xIdx, grid.yIdy),對應上圖例子就是(1, 1),通過它我們就能找到了這個線程塊的位置,然後我們啓動二級索引(block.xIdx, block.yIdx, block.zIdx)來定位到指定的線程。這就是CUDA的線程組織結構。

  • 每個線程由每個線程處理器(SP)執行
  • 線程塊由多核處理器(SM)執行
  • 一個kernel其實由一個grid來執行,一個kernel一次只能在一個GPU上執行

block是軟件概念,一個block只會由一個sm調度,程序員在開發時,通過設定block的屬性,告訴GPU硬件,我有多少個線程,線程怎麼組織。而具體怎麼調度由sm的warps scheduler負責,block一旦被分配好SM,該block就會一直駐留在該SM中,直到執行結束。一個SM可以同時擁有多個blocks,但需要序列執行。

3. CUDA內存模型

CUDA中的內存模型分爲以下幾個層次:

  • 每個線程都用自己的registers(寄存器)
  • 每個線程都有自己的local memory(局部內存)
  • 每個線程塊內都有自己的shared memory(共享內存),所有線程塊內的所有線程共享這段內存資源
  • 每個grid都有自己的global memory(全局內存),不同線程塊的線程都可使用
  • 每個grid都有自己的constant memory(常量內存)和texture memory(紋理內存),),不同線程塊的線程都可使用

線程訪問這幾類存儲器的速度是register > local memory >shared memory > global memory

4. CUDA編程模型

1、怎麼寫一個能在GPU跑的程序或函數

通過關鍵字就可以表示某個程序在CPU上跑還是在GPU上跑!如下表所示,比如我們用__global__定義一個kernel函數,就是CPU上調用,GPU上執行,注意__global__函數的返回值必須設置爲void。

2、CPU和GPU間的數據傳輸怎麼寫

首先介紹在GPU內存分配回收內存的函數接口:

  • cudaMalloc(): 在設備端分配global memory
  • cudaFree(): 釋放存儲空間

CPU的數據和GPU端數據做數據傳輸的函數接口是一樣的,他們通過傳遞的函數實參(枚舉類型)來表示傳輸方向:

cudaMemcpy(void dst, void src, size_t nbytes,
enum cudaMemcpyKind direction)

enum cudaMemcpyKind:

  • cudaMemcpyHostToDevice(CPU到GPU)
  • cudaMemcpyDeviceToHost(GPU到CPU)
  • cudaMemcpyDeviceToDevice(GPU到GPU)

3、怎麼用代碼表示線程組織模型

可以用dim3類來表示網格和線程塊的組織方式,網格grid可以表示爲一維和二維格式,線程塊block可以表示爲一維、二維和三維的數據格式。

dim3 DimGrid(100, 50);  //5000個線程塊,維度是100*50
dim3 DimBlock(4, 8, 8);  //每個線層塊內包含256個線程,線程塊內的維度是4*8*8

 

這裏說明下我的環境配置

  • ubuntu18.04
  • cuda10.1
  • cudnn7.6

 

二、nvcc編譯CUDA

nvcc是編譯cuda程序的編譯器,CDUA C是在C語言上的擴展,所以它依賴C編譯器(在Linux下是gcc)。因此我們編譯CUDA程序必須依靠編譯器nvcc。其實,nvcc編譯cuda程序和g++編譯c++程序是差不多的。

example

示例中創建了一個main.cu作爲主程序入口,foo.cuh和foo.cu定義了一個函數實現。注意到文件的後綴都是cuda程序可識別的後綴。

main.cu:

#include <stdio.h>
#include <iostream>
#include <cuda_runtime.h>
#include "foo.cuh"

int main()
{
    std::cout<<"Using NVCC"<<std::endl;
    useCUDA();
    return 0;
}

foo.cuh:

#ifndef FOO_CUH
#define FOO_CUH

#include <stdio.h>

__global__ void foo();

extern "C" 
void useCUDA();

#endif

foo.cu: 這裏的執行配置運算符<<< >>>,用來傳遞內核函數的執行參數。執行配置有四個參數,第一個參數聲明網格的大小,第二個參數聲明塊的大小,第三個參數聲明動態分配的共享存儲器大小,默認爲 0,最後一個參數聲明執行的流,默認爲 0

#include "foo.cuh"

#define CHECK(res) { if(res != cudaSuccess){printf("Error :%s:%d , ", __FILE__,__LINE__);   \
printf("code : %d , reason : %s \n", res,cudaGetErrorString(res));exit(-1);}}

__global__ void foo()
{
    printf("CUDA!\n");
}


void useCUDA()
{
    foo<<<1,5>>>();
    CHECK(cudaDeviceSynchronize());
}

在代碼所在目錄,命令行輸入下行命令即可完成cuda程序的編譯。

nvcc -o main.out main.cu foo.cu

運行:./main.out 可得:

 

三、CMake混合編譯C++與CUDA

在圖像處理中,許多c/c++的項目會使用cuda加速其算法,c/c++有其編譯器:gcc/g++,cuda有其編譯器nvcc。一般採用gcc/g++編譯c/c++部分代碼,nvcc編譯cuda代碼部分,即分離式編譯。

分離式編譯的基本思路:
將cuda部分寫成接口:

void API(){...}

在c/c++代碼中通過添加接口聲明:

extend "C" void API(){...}

這樣g++編譯器就可以只處理C++有關代碼,含有cuda代碼的部分就由nvcc處理。

除添加聲明外,要真正實現調用並完成功能,需要有真正的函數實現,即要將CUDA接口製作成靜態庫或動態庫,c++通過調用聲明並調用庫中的實現來完成所需功能。

下面的例子使用CMake工具,將cuda部分作爲一個項目並製作爲庫,由其主項目調用。

example

 c_cuda目錄的結構如下:

c_cuda/
--main.cpp
--CMakeLists.txt
--cuda/
  --foo.cuh
  --foo.cu
  --CMakeLists.txt

C++主程序:main.cpp

#include <stdio.h>
#include <iostream>

extern "C"
void useCUDA();

int main()
{
    std::cout<<"Using C++"<<std::endl;
    useCUDA();
    return 0;
}

main.cpp所在目錄下的CMakeLists.txt:

# CMakeLists.txt for G4CU project

project(project)

# required cmake version
cmake_minimum_required(VERSION 2.8)

add_subdirectory(cuda)
set (EXTRA_LIBS ${EXTRA_LIBS} gpu)

ADD_EXECUTABLE(project main.cpp)

target_link_libraries (project ${EXTRA_LIBS})

foo.cuh:

#ifndef FOO_CUH
#define FOO_CUH

#include <stdio.h>

extern "C" 
void useCUDA();

#endif

foo.cu:

#include "foo.cuh"

#define CHECK(res) { if(res != cudaSuccess){printf("Error :%s:%d , ", __FILE__,__LINE__);   \
printf("code : %d , reason : %s \n", res,cudaGetErrorString(res));exit(-1);}}

__global__ void foo()
{
    printf("CUDA!\n");
}

void useCUDA()
{
    foo<<<1,5>>>();
    CHECK(cudaDeviceSynchronize());
}

cuda目錄下的CMakeLists.txt:  最後兩行的SHARED 表示生成動態庫,STATIC表示生成靜態庫。

# CMakeLists.txt for G4CU project

project(gpu)

# required cmake version
cmake_minimum_required(VERSION 2.8)

# packages
find_package(CUDA)

#include_directories ("${PROJECT_SOURCE_DIR}")

# nvcc flags
set(CUDA_NVCC_FLAGS -O3;-G;-g)

#set(CUDA_NVCC_FLAGS -gencode arch=compute_20,code=sm_20;-G;-g)
#set(CUDA_NVCC_FLAGS -gencode arch=compute_52,code=sm_52;-G;-g)

file(GLOB_RECURSE CURRENT_HEADERS  *.h *.hpp *.cuh)
file(GLOB CURRENT_SOURCES  *.cpp *.cu)

source_group("Include" FILES ${CURRENT_HEADERS}) 
source_group("Source" FILES ${CURRENT_SOURCES}) 

#cuda_add_library(gpu SHARED ${CURRENT_HEADERS} ${CURRENT_SOURCES})
cuda_add_library(gpu STATIC ${CURRENT_HEADERS} ${CURRENT_SOURCES})

編譯運行

cmake .
make
./project

 

參考:

[1] CUDA編程之快速入門

[2] CUDA編譯(一)—使用nvcc編譯cuda

[3] Ubuntu下通過CMake文件編譯CUDA+OpenCV代碼操作步驟

[4] 資料查詢 https://docs.nvidia.com/cuda/

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