算法優化系列筆記——cuda入門

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

我們來分析一下,整個程序執行的過程。整個執行過程可以分爲下面四個步驟:

step 1: 自動分配內存
step 2: 數據初始化
step 3:執行運算程序
step 4:自動釋放內存

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

我們再次來分析一下,手動管理內存執行的過程。整個執行過程可以分爲下面四個步驟:

step 1: 手動分配內存
step 2: 數據初始化
step 3:執行運算程序
step 4:手動釋放內存

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 數組加法的過程

step 1.1: 分配host內存
step 1.2: 進行數據初始化
step 2.1: 分配device內存
step 2.2: 從host將數據拷貝到device上
step 3: 調用CUDA的核函數在device上完成指定的運算
step 4: 將device上的運算結果拷貝到host上
step 5: 釋放device和host上分配的內存

我們發現主要增加了三項內容,一、給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計算的主要思想和第一個能完全運行起來的程序。在實踐中學習,纔是進步最快的,在下一篇筆記中,我們將解釋下各個參數設置的技巧,以及我們如何在程序中使用這些加速算法。

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