Cuda on Arm 冬令营第一天学习内容

简述

今天的主要学习内容包括课程说明,Cuda和Gpu简介,简单的Linux操作,简单的Cuda编程。

简单复习几个并行化指标、MakeFile和Cuda编程内容

并行化指标--加速比

加速比的理解

加速比主要是为了对比并行Gpu计算的情况下比串行Cpu计算代码速度有多大的提升,其计算公式在下图中:

按照文字进行理解,假定有80%的内容可以进行并行话,那么

Speedup = 1 / (1 - 0.8) = 5

对于并行的加速比计算公式如下:

加速比的极限

加速比是存在上限的,根据上图我们可以知道,当N越大的时候,P/N就会越小,也就表示分母越小,从而加速比越大。

但是,P/N也受到P的制约,由于S + P = 1,所以S的大小也决定着加速比的上限,按照p=.50p=.90p=.99进行加速比计算举例,结果如图:

可以看到加速比是没能随着N的增加而继续增加的。

MakeFile

个人理解

MakeFile是编译C,C++的一个文件,按照MakeFile内的内容对部分的文件进行编译或者文件的生成。

简单指令示例

使用make执行MakeFile文件。如果MakeFile内写有Clean等内容,可以使用make clean执行清理操作

OBJ = main.o add.o sub.o mul.o div.o
CC = gcc

app: $(OBJ)
    $(CC) -o app $(OBJ)

main.o: main.c
    $(CC) -c main.c

add.o: add.c
    $(CC) -c add.c

sub.o:sub.c
    $(CC) -c sub.c

mul.o: mul.c
    $(CC) -c mul.c

div.o: div.c
    $(CC) -c div.c

.PHONY : clean
clean :
    -rm $(OBJ) app    

从上面的代码可以看出,临时变量写在最开始,使用的时候使用$()进行包裹指代,而clean应该写在最后,以免影响make指令的执行顺序。

其中的.PHONY : clean中应该关注.PHONY这一项,因为这个关键字主要是为了避免指令和文件名冲突,假如目录下同样存在clean文件,那么make clean在没有.PHONY的情况下会报错(可以自行尝试)。

另外需要注意,换行之后尽量使用TAB进行缩进。

Cuda代码示例

课件中同样提供了Cuda编译使用的一个示例:

TEST_SOURCE = hello_cuda.cu

TARGETBIN := ./hello_cuda

CC = /usr/local/cuda/bin/nvcc

$(TARGETBIN):$(TEST_SOURCE)
    $(CC)  $(TEST_SOURCE) -o $(TARGETBIN)

.PHONY:clean
clean:
    -rm -rf $(TARGETBIN)    

可以看到基本的内容和上面是一样的,只是编译的文件从xxx.c变成了xxx.cu,编译指令从gcc变成了nvcc。其他并无不同。

Cuda编程初识

课程内容比较多,就简单挑着自己的理解去写了。

数据传输

在进行代码之前,先对cuda的数据传输进行一定的了解,由于是操作GPU进行计算,所以主要的数据其实都应该导入到GPU的缓存中进行,包括部分指令也会在GPU上操作。

所以可以看到如下图所示:

在运行执行结束之后,运算结果讲重新搬回CPU中,进行下一步的操作。如下图:

所以这几步操作会体现在代码中会有一定的复制、转移的操作,同时内存申请和释放也会比单纯的串行程序使用更多一步GPU的内存申请和释放。

运行位置

在CUDA编程中,程序执行和调用的位置会有两个,一个是CPU(也称Host),另外一个则是GPU(也称Device)。函数模式,则一共有三种,分别是:__device____global____host__。先看下图:

根据上图我们可以看到__device__很明显应该是在GPU设备中被调用然后执行,而__global__则是从CPU到GPU的过度函数,甚至就是使用GPU的函数,而__host__则是单纯的CPU函数,也就是常规的C函数。

CUDA程序编译

在上面的MakeFile中已经进行了简单的差异描述,但是,为了保证编译后的文件的可用,GPU编译比CPU编译多了一个参数,通常是为了指定架构,例如arch = 60,由于走神,这里没太懂,所以放一下PPT图片:

PPT

NVPROF:程序分析软件

主要是三种模式,每种模式都有不同的显示内容:

nvprof a.exe

nvprof --print-gpu-trace a.exe

nvprof --print-api-trace a.exe

CUDA线程调度

在线程调度这一部分,主要有三个层级GridBlockThread,其中Thread是常见的线程,而Block是为了管理Thread而出现的,在常规的GPU core中,一个Block通常拥有32个Thread

Grid则是管理Block的,在线程层次和线程调度入门中,个人认为有两个地方较为重要:一是线程的组织结构,二是线程索引。

组织结构

Thread开始,首先我们应该注意,Thread会被wrap调度器以32个为一组,组成一个wrap,但是Block才是管理thread的,所以Block_si ze应该是以32作为倍数,比如32或者64,以求最大化使用算力。

而接下来,由Thread组成了Block,在基础的情况中中,<<<BLOCKS,BLOCK_SIZE>>>表示的是申请BLOCKSBlock,每个Block内部有
BLOCK_SIZEThread,这时候情况较为简单,Grid的应该是一个1XBLOCKS的长矩阵,每个Block也是一个1xBLOCK_SIZE的长矩阵。

接下来是由dim3构成的高纬度线程模型,举例来说,我们可以由dim3 grid_size(3,3,3)定义出一个3x3x3的三维矩阵,在这个矩阵中,一共有27个Block
此时,这个Grid的维度,也就是gridDim.xgridDim.ygridDim.z三个值,均为3,也就是blockIdx的x、y、z最大值均为2(下标从0开始)。
在这样时候,你可以把整个Grid视为一个常见的3x3的魔方,魔方上面的每一个方块都是一个Block

而我们再定义一个dim3 block_size(3,3,3)之后,每个Block也变成了一个3x3的小魔方,此时每个Thread是组成这个魔方的一份子。

然后使用<<<grid_size,blocksize>>,我们得到的线程总数应该是729个(是 3x3x3 * 3x3x3的结果)。

线程索引

先看一句求线程索引的代码(注意,此时cuda的调度维度是<<<BLOCKS,BLOCK_SIZE>>>):

    int n = blockDim.x * blockIdx.x + threadIdx.x;

其中,Dim 表示的是维度,例如Block是一个10x10的矩阵,那么BlockDim.xBlockDim.y都应该是10。

Idx则表示索引,也就是所在位置。例如Grid组成了一个5x4的矩阵,而某个Block位于(3,2)位置,那么BlockIdx.x是2,BlockIdx.y是3。

所以我们再来看n的求得过程,其中blockDim.x * blockIdx.x作用是求得前面的Block一共占了多少个位置,然后再加上在Block内部ThreadIdx.x
来求得自己所在位置。

接下来重新读Idx的那段话,一个很奇怪的地方在于,X是2,而Y是3,这里应该这样思考,X表示的是横坐标,但是放到矩阵上,表示的其实是第几列,而Y则相反,
表示的是纵坐标,在矩阵上表达的是第几行,所以在求一个线程在一个巨型矩阵中的位置的时候,列坐标应该是用

row = threadIdx.x + BlockIdx.x* BlockDim.x

而行坐标则是应该用

col = threadIdx.y + BlockIdx.y * BLockDim.y

CUDA操作代码

接下来是一个Cuda的向量相加的代码示范:

#include <math.h>
#include <stdio.h>

/***
 * 
 */
void __global__ add(const double *x, const double *y, double *z, int count)
{
    //根据所处的block和block内的线程号来确定自己处理的是数组中的哪一个值
    const int n = blockDim.x * blockIdx.x + threadIdx.x;
    if( n < count)
    {
        z[n] = x[n] + y[n];
    }

}

/***
 * 
 */
void check(const double *z, const int N)
{
    bool error = false;
    for (int n = 0; n < N; ++n)
    {
        if (fabs(z[n] - 3) > (1.0e-10))
        {
            error = true;
        }
    }
    printf("%s\n", error ? "Errors" : "Pass");
}

/***
 * 
 */
int main(void)
{
    const int N = 100000000;
    const int M = sizeof(double) * N;
    double *h_x = (double*) malloc(M);
    double *h_y = (double*) malloc(M);
    double *h_z = (double*) malloc(M);

    for (int n = 0; n < N; ++n)
    {
        h_x[n] = 1;
        h_y[n] = 2;
    }
    //申请GPU内存并且进行数据拷贝
    double *d_x, *d_y, *d_z;
    cudaMalloc((void **)&d_x, M);
    cudaMalloc((void **)&d_y, M);
    cudaMalloc((void **)&d_z, M);
    // cudaMemcpuHostToDevice 表示数据从CPU转向GPU
    cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice);
    cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice);
    // 
    const int block_size = 128;
    const int grid_size = (N + block_size - 1) / block_size;
    // 调用GPU进行并行计算,这里是一个一维的grid和一维的block,分别都只有x
    add<<<grid_size, block_size>>>(d_x, d_y, d_z,N);
    // cudaMemDeviceToHost 表示数据从GPU转向CPU
    cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost);
    check(h_z, N);

    free(h_x);
    free(h_y);
    free(h_z);
    //进行CUDA内存释放
    cudaFree(d_x);
    cudaFree(d_y);
    cudaFree(d_z);
    return 0;
}

为了更清楚的对比,在下面放上了单纯CPU的向量相加的写法:

#include <math.h>
#include <stdlib.h>
#include <stdio.h>

void add(const double *x, const double *y, double *z, const int N)
{
    for (int n = 0; n < N; ++n)
    {
        z[n] = x[n] + y[n];
    }
}

void check(const double *z, const int N)
{
    bool has_error = false;
    for (int n = 0; n < N; ++n)
    {
        if (fabs(z[n] - 3) > (1.0e-10))
        {
            has_error = true;
        }
    }
    printf("%s\n", has_error ? "Errors" : "Pass");
}


int main(void)
{
    const int N = 100000000;
    const int M = sizeof(double) * N;
    double *x = (double*) malloc(M);
    double *y = (double*) malloc(M);
    double *z = (double*) malloc(M);

    for (int n = 0; n < N; ++n)
    {
        x[n] = 1;
        y[n] = 2;
    }

    add(x, y, z, N);
    check(z, N);

    free(x);
    free(y);
    free(z);
    return 0;
}

结尾

困了困了,后面的重点写的有点敷衍了。明天有精力再修改一下。