1 引言
算法在工程化過程中,最躲不開就是算法的優化問題。優化分很多個方向,最簡單的實現方式是並行化加速。如:一個向量相加,在cpu中你是串行一個元素一個元素的加減,如果採用並行化加速,你可以一次操作,可以將向量加法完成。當然,並行化的實現方式大概分兩類:(1) cpu多線程的方法,如:openmp,(2)異構計算的方法。如gpu加速,fpga加速,NPU加速等。cpu多線程的方法的必要條件就是你得有足夠的計算資源,往往在自動駕駛等任務中,cpu往往是稀缺資源。所以異構計算是最常被使用的方法,而異構計算中,沒有比N卡的cuda框架更通用的了。爲此,我們總結一下cuda的使用方法。
說明:cuda的安裝和配置網上資源比較多,暫時先不寫了。等以後有時間再補上吧。強調一下gpu驅動版本一定要和cuda版本相對應,不然沒法運行
檢查下環境
nvcc --version
#輸出類似
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2015 NVIDIA Corporation
Built on Tue_Aug_11_14:27:32_CDT_2015
Cuda compilation tools, release 7.5, V7.5.17
我們以一個例子,作爲入門課程。而不是一上來就來一堆硬件知識,直接把你幹懵逼。
2 cpu版的數組加法
2.1 自動內存管理
cpu_arr_add.cpp
#include <stdio.h>
int main(){
//step1
int num = 10;
int a[num],b[num],c[num];
//step2
for(size_t i=0; i<num; ++i){
a[i] = i;
b[i] = 2*i;
}
//step3
for(size_t i=0; i<num; ++i){
c[i] = a[i] + b[i];
}
for(size_t i=0; i<num; ++i){
printf("%d + %d = %d\n", a[i], b[i], c[i]);
}
//step4
return 0;
}
採用gcc編譯一下
gcc cpu_arr_add.cpp -o cpu_arr_add
運行一下
./cpu_arr_add
0 + 0 = 0
1 + 2 = 3
2 + 4 = 6
3 + 6 = 9
4 + 8 = 12
5 + 10 = 15
6 + 12 = 18
7 + 14 = 21
8 + 16 = 24
9 + 18 = 27
我們來分析一下,整個程序執行的過程。整個執行過程可以分爲下面四個步驟:
step4 需要稍微理解一下,a[i],b[i],c[i]在程序執行完後,系統會自動釋放。
2.2 手動管理內存
cpu_arr_add_malloc.cpp
#include <stdio.h>
#include <stdlib.h>
int main(){
int num = 10;
int *a,*b,*c;
//step1
a = (int *) malloc(num * sizeof(int));//c 語言中,手動分配內存
b = (int *) malloc(num * sizeof(int));
c = (int *) malloc(num * sizeof(int));
//step2
for(size_t i=0; i<num; ++i){
a[i] = i;
b[i] = 2*i;
}
//step3
for(size_t i=0; i<num; ++i){
c[i] = a[i] + b[i];
}
for(size_t i=0; i<num; ++i){
printf("%d + %d = %d\n", a[i], b[i], c[i]);
}
//step4
free(a);//釋放內存
free(b);
free(c);
return 0;
}
採用gcc編譯一下
gcc cpu_arr_add_malloc.cpp -o cpu_arr_add_malloc
運行一下
./cpu_arr_add_malloc
0 + 0 = 0
1 + 2 = 3
2 + 4 = 6
3 + 6 = 9
4 + 8 = 12
5 + 10 = 15
6 + 12 = 18
7 + 14 = 21
8 + 16 = 24
9 + 18 = 27
我們再次來分析一下,手動管理內存執行的過程。整個執行過程可以分爲下面四個步驟:
3 cuda版數組加法
vector_add.cu
#include <stdio.h>
__global__ void arr_add(int* a,int* b,int* c,int num){
int i = threadIdx.x;
if(i<num){
c[i] = a[i] +b[i];
}
}
/**
典型的CUDA程序的執行流程如下:
step 1: 分配host內存,並進行數據初始化;
step 2: 分配device內存,並從host將數據拷貝到device上;
step 3: 調用CUDA的核函數在device上完成指定的運算;
step 4: 將device上的運算結果拷貝到host上;
step 5: 釋放device和host上分配的內存。
*/
int main(){
//step 1.1: 分配host內存
int num = 10;
int a[num],b[num],c[num];
int *a_gpu, *b_gpu, *c_gpu;
//step 1.2: 進行數據初始化
for(size_t i=0; i<num; ++i){
a[i] = i;
b[i] = 2*i;
}
//step 2.1: 分配device內存
cudaMalloc((void **)&a_gpu, num*sizeof(int));
cudaMalloc((void **)&b_gpu, num*sizeof(int));
cudaMalloc((void **)&c_gpu, num*sizeof(int));
//step 2.2: 從host將數據拷貝到device上
cudaMemcpy(a_gpu,a,num*sizeof(int),cudaMemcpyHostToDevice);
cudaMemcpy(b_gpu,b,num*sizeof(int),cudaMemcpyHostToDevice);
//step 3: 調用CUDA的核函數在device上完成指定的運算;
arr_add<<<1,10>>>(a_gpu,b_gpu,c_gpu,num);
//step 4: 將device上的運算結果拷貝到host上;
cudaMemcpy(c,c_gpu,num*sizeof(int),cudaMemcpyDeviceToHost);
for(size_t i=0; i<num; ++i){
printf("%d + %d = %d\n", a[i], b[i], c[i]);
}
//step 5: 釋放device和host上分配的內存
cudaFree(a_gpu);
cudaFree(b_gpu);
cudaFree(c_gpu);
return 0;
}
nvcc vector_add.cu -o vec_add
./vec_add
0 + 0 = 0
1 + 2 = 3
2 + 4 = 6
3 + 6 = 9
4 + 8 = 12
5 + 10 = 15
6 + 12 = 18
7 + 14 = 21
8 + 16 = 24
9 + 18 = 27
有沒有看到似曾相識的感覺。只是同樣內存分配了兩次,還有內存拷貝,還有兩次內存釋放
有沒有覺得看不懂,有些內容不認識,如:cudaMalloc
,cudaMemcpy
,cudaMemcpyHostToDevice
還有__global__
,<<<>>>
這個三個尖括號是什麼鬼
在講解這個問題前,我們來解析兩個概念。我們電腦上現在有兩個計算單元,一個CPU和一個GPU。我們來分析一下異同。
內容 | CPU | GPU |
---|---|---|
異構計算中名稱 | Host | Device |
內存分配 | malloc | cudaMalloc |
內存釋放 | free | cudaFree |
運行方式 | 串行 | 並行 |
標示符 | __host__ |
__global__ |
編譯工具 | gcc,g++ | nvcc |
我們來看下,GPU 數組加法的過程
我們發現主要增加了三項內容,一、給device(GPU)分配內存;二、將數據從host(CPU)拷貝到device(GPU);三、將計算結果從device拷貝到host。這是因爲計算是在GPU上完成的。
我們終於到了,我們神奇的<<<>>>
了,這個是幹嘛的了?
這就是我們爲啥要用gpu加速的計算的原因了,我們前面說了gpu是並行計算,到底怎麼實現的了,關鍵就在這個符號內傳入的參數。arr_add<<<1,10>>>(a_gpu,b_gpu,c_gpu,num);
在我們的程序中,我相當於用了1x10的線程並行計算。而這兩個參數到底是怎麼設置的,我們下個筆記詳細說明。
4 總結分析
我們來分析下cpu和gpu運算之間的區別
//cpu中
//step3
for(size_t i=0; i<num; ++i){
c[i] = a[i] + b[i];
}
//gpu中
__global__ void arr_add(int* a,int* b,int* c,int num){
int i = threadIdx.x;
if(i<num){
c[i] = a[i] +b[i];
}
}
//step 3: 調用CUDA的核函數在device上完成指定的運算;
arr_add<<<1,10>>>(a_gpu,b_gpu,c_gpu,num);
在cpu中我們用for循環,執行完一圈然後回去再執行下一圈
在gpu中我調用了__global__ void arr_add
函數,發現for循環消失了。而是同時開了10個線程同時進行計算。
5 拔
我們主要解釋下cuda計算的主要思想和第一個能完全運行起來的程序。在實踐中學習,纔是進步最快的,在下一篇筆記中,我們將解釋下各個參數設置的技巧,以及我們如何在程序中使用這些加速算法。