前言
隨著深度學習的發展,AI算法對計算的需求量越來越大,傳統的CPU串行編程已經不能滿足企業對AI低延遲高性能要求, GPU并行編程越來越受到關注, 因此掌握一門GPU并行編程技術對于AI軟件棧開發的人員非常必要.
關于GPU編程
目前Server端主流的GPU大部分采用NVIDIA GPU, 例如V100, A100等系列, 也有部分采用AMD 系列GPU, NVIDIA以及AMD都為GPU編程提供了相應的軟件開發工具以及框架.
- NVIDIA: CUDA
- AMD: HIP
筆者對AMD GPU的軟件棧略知一二, AMD的HIP編程基本上和NVIDIA CUDA非常相似, 目前已經有相應的工具可以將AMD HIP與NV CUDA的代碼進行相互轉換, 例如PyTorch提供的Hipfiy工具.
測試環境
- OS: Ubuntu 20.04
- CUDA: v11.4
- GCC: 10.3
- Docker: v20
- VSCode
Ubuntu CUDA開發環境快速搭建
Ubuntu上搭建CUDA開發環境有2種方式:
- NVIDIA官網下載CUDA安裝包, 執行安裝腳本
- 采用NVIDIA提供的CUDA docker環境, 開箱即用
在公司和企業中, 由于不同人員往往會交叉使用服務器資源,因此docker應用的比較廣泛, docker可以提供一個標準化, 可復現的統一環境. 因此筆者決定采用NVIDIA提供的docker進行CUDA開發環境的創建.
Docker 環境檢查
- 首先需要確保Ubuntu系統是否安裝了docker:
docker --version
, 為了方便使用GPU, 選擇docker的版本>19 - 安裝: nvidia-docker2
輸出結果:
Docker version 20.10.11, build dea9396
獲取NVIDIA CUDA docker
DockerHub提供了 nvidia/cuda
的docker 鏡像:
- dockerHub: https://registry.hub.docker.com/
image.png - nvidia/cuda鏡像: 在dockerHub中搜索nvidia/cuda: https://registry.hub.docker.com/r/nvidia/cuda/tags
image.png
nvidia/cuda針對x86, ARM64等提供了各個版本的docker鏡像, nvidia/cuda中的docker鏡像主要包含3中不同的鏡像:
hree flavors of images are provided:
base
: Includes the CUDA runtime (cudart)runtime
: Builds on thebase
and includes the [CUDA math libraries](https://developer.nvidia.com/gpu-> accelerated-libraries), and NCCL. Aruntime
image that also includes cuDNN is available.devel
: Builds on theruntime
and includes headers, development tools for building CUDA images. These images are particularly useful for multi-stage builds.
由于nvidia/docker提供了多種docker鏡像, 因此我們根據自己的需求選擇一個合適版本/處理器架構的docker鏡像, 以 Ubuntu 20.04為例:
- 11.4.2-devel-ubuntu20.04
- 11.4.2-runtime-ubuntu20.04 包含cuda-base + CUDA 數學加速庫(比如cublas, cufft) + cudnn
- 11.4.2-base-ubuntu20.04
- 11.4.2-cudnn8-runtime-ubuntu20.04
- 11.4.2-cudnn8-devel-ubuntu20.04 devel版本的docker鏡像是基于runtime鏡像創建的
筆者選擇了一個比較全的docker鏡像: 11.4.2-cudnn8-devel-ubuntu20.04
docker鏡像下載: 在ubuntu終端中輸入: docker pull nvidia/cuda:11.4.2-cudnn8-devel-ubuntu20.04
下載完成之后, 可以查看docker鏡像: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nvidia/cuda 11.4.2-cudnn8-devel-ubuntu20.04 b1539d83387e 3 months ago 9.14GB
創建CUDA Docker容器
Docker容器: docker容器是docker鏡像的實例化, docker鏡像運行之后的產物; 類似于進程和程序的概念, 程序是靜態的代碼, 進程是程序載入內存之后運行態的程序.
下載好 nvidia/docker
鏡像之后, 開始啟動一個docker容器, 并且進入docker:
簡單的命令: docker run -it --name=test-cuda --gpus=all nvidia/cuda:11.4.2-cudnn8-devel-ubuntu20.04
不出意外, docker容器創建成功并且自動進入了docker, 檢查一下環境:
nvidia-smi # 查看GPU
nvcc --version # 查看CUDA編譯器版本
工程測試
Ubuntu中CUDA 的安裝位置說明
一般情況下CUDA默認安裝的目錄: /usr/local/cuda
, 存在如下目錄:
- bin: 二進制目錄,包含nvcc, nvprof. cuda-gdb等相關工具
- extras
- nsight-compute-2020.2.0
- nvvm
- src
- compute-sanitizer
-
include: CUDA提供的C/C++ 頭文件, 例如:
cuda_runtime.h
- nsightee_plugins
- README
- targets
- DOCS
- lib64: CUDA提供的so動態庫
- nsight-systems-2020.3.4
- samples: CUDA演示的例子
- tools
- EULA.txt
- libnvvp
- nvml
- share
基于cmake 的簡單CUDA測試程序
測試程序的功能: 兩個數組簡單相加, element-wise add
- main.cu
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cuda_runtime.h>
void elmtwise_sum_cpu(int* arr1, int* arr2, int* out, int N) {
for(int i=0;i<N;i++) out[i] = arr1[i] + arr2[i];
}
__global__ void kernel_sum(int* arr1, int* arr2, int* out, int N) {
int thread_id = blockIdx.x * blockDim.x + threadIdx.x;
if(thread_id < N) {
out[thread_id] = arr1[thread_id] + arr2[thread_id];
}
}
void elmtwise_sum_gpu(int* arr1, int* arr2, int* out, int N) {
// 1. GPU端申請顯存
int* d_arr1 = nullptr;
int* d_arr2 = nullptr;
int* d_out = nullptr;
cudaMalloc((void**)&d_arr1, sizeof(int)*N);
cudaMalloc((void**)&d_arr2, sizeof(int)*N);
cudaMalloc((void**)&d_out, sizeof(int)*N);
// 2. CPU Memory數據復制到GPU顯存
cudaMemcpy(d_arr1, arr1, sizeof(int)*N, cudaMemcpyHostToDevice);
cudaMemcpy(d_arr2, arr2, sizeof(int)*N, cudaMemcpyHostToDevice);
// 3. 設置GPU端線程配置, launch the GPU kernel
int blk_size = 128;
int grid_size = (N + blk_size -1) / blk_size;
kernel_sum<<<grid_size, blk_size>>>(d_arr1, d_arr2, d_out, N);
// 4. Cpoy GPU result to CPU
cudaMemcpy(out, d_out, sizeof(int)*N, cudaMemcpyDeviceToHost);
// 5. Free GPU Memory
cudaFree(d_arr1);
cudaFree(d_arr2);
cudaFree(d_out);
}
int main() {
const int N = 512* 512;
int* arr1 = new int[N];
int* arr2 = new int[N];
int* out_cpu = new int[N];
int* out_gpu = new int[N];
srand(123456);
for(int i=0;i<N;i++) {
arr1[i] = rand() * 5 % 255;
arr2[i] = rand() % 128 + 5;
}
elmtwise_sum_cpu(arr1, arr2, out_cpu, N);
elmtwise_sum_gpu(arr1, arr2, out_gpu, N);
auto print_array = [](int* arr, int N, int k, const std::string& msg) -> void {
std::cout << msg << std::endl;
int n = std::min(N, k);
for(int i=0;i<n;i++) std::cout << arr[i] <<" ";
std::cout << std::endl;
};
print_array(out_cpu, N, 10, "CPU");
print_array(out_gpu, N, 10, "GPU");
// validate
int i=0;
for(i=0;i<N;i++){
if(out_cpu[i] != out_gpu[i]){
std::cout << "Error, not equal!" << std::endl;
break;
}
}
if(i==N) std::cout << "Test OK, all correct !" << std::endl;
delete[] arr1;
delete[] arr2;
delete[] out_cpu;
delete[] out_gpu;
return 0;
}
- CMakeLists.txt
project(TEST_CUDA LANGUAGES CXX CUDA)
cmake_minimum_required(VERSION 3.10)
# https://zhuanlan.zhihu.com/p/105721133
if(CUDA_ENABLE)
enable_language(CUDA)
endif()
add_executable(main "main.cu")
編譯 & run:
mkdir -p build
cd build
cmake ../
make -j8
# run
./main
運行結果:5
程序分析:
典型的GPU程序執行流程: GPU端申請內存 ---> Copy data from CPU to GPU ---> Launch GPU kenrel ---> Copy result from GPU to CPU ---> Free GPU Memory
CUDA編程頭文件:
<cuda_runtime.h>
, 包含常用的CUDA函數, 例如cudaMalloc(), cudaMemcpy() 用于在顯存分配空間以及CPU-GPU端數據拷貝傳輸
__global__ void kernel_sum
: GPU上執行的核函數, kernel function,__global__
修飾符表示此函數是一個GPU kernel function, 次函數在CPU端調用,在GPU端執行GPU端線程配置
// 3. 設置GPU端線程配置, launch the GPU kernel
int blk_size = 128; --- block_size, 代表1個block中CUDA線程的數量,一般為2的冪數
int grid_size = (N + blk_size -1) / blk_size; --- gride_size: 代表全部計算需要的block個數, 注意這里需要向上取整
kernel_sum<<<grid_size, blk_size>>>(d_arr1, d_arr2, d_out, N); --- <<< grid_size, blk_size>>> CUDA特有的kernel啟動方式