【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/

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