CUDA学习笔记(2)—— GPU

CUDA学习笔记(2)—— GPU

虽然GPU具有强大的算力,但GPU并不是一个独立运行的计算平台,而需要与CPU协同工作,可以看成是CPU的协处理器,因此当我们在说GPU并行计算时,其实是指的基于CPU+GPU的异构计算架构。在异构计算架构中,GPU与CPU通过PCIe总线连接在一起来协同工作。
在这里插入图片描述
GPU的存储体系根据GPU的类型不同,可以是逻辑上的,也可以是物理上的。对于集成显卡(即integrated GPU)而言,例如 Intel HD Graphics ,GPU和CPU位于同一die中,所以它没有自己的物理存储设备,而是共享CPU的存储空间,即Unified Memory Architecture(一致性存储架构),通常是从CPU的存储中划分一部分出来作为该GPU的local memory;另一种显卡称为独立显卡(即dedicated GPU),像Nvidia和AMD生产的GPU就属于这类,它们都拥有自己的物理存储设备,是我们日常使用最多的GPU类型了。无论哪种GPU,它都拥有自己的一套地址空间,且独立于CPU的虚拟内存地址空间。GPU地址空间的管理是通过内核态驱动来完成的,例如Windows上的KMD(Kernel-Mode Driver)。

CPU大部分面积为控制器和寄存器,与之相比,GPU拥有更多的ALU(ArithmeticLogicUnit,逻辑运算单元)用于数据处理,而非数据高速缓存和流控制,这样的结构适合对密集型数据进行并行处理。CPU执行计算任务时,一个时刻只处理一个数据,不存在真正意义上的并行,而GPU具有多个处理器核,在一个时刻可以并行处理多个数据。从实际来看,CPU芯片空间的5%是ALU,而GPU空间的40%是ALU。这也是导致GPU计算能力超强的原因。GPU面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。

GPU包括更多的运算核心,其特别适合数据并行的计算密集型任务,如大型矩阵运算,而CPU的运算核心较少,但是其可以实现复杂的逻辑运算,因此其适合控制密集型任务。另外,CPU上的线程是重量级的,上下文切换开销大,但是GPU由于存在很多核心,其线程是轻量级的。因此,基于CPU+GPU的异构计算平台可以优势互补,CPU负责处理逻辑复杂的串行程序,而GPU重点处理数据密集型的并行计算程序,从而发挥最大功效。

举个例子,假设有一堆相同的加减乘除计算任务需要处理,那把这个任务交给一堆(几十个)小学生就可以了,这里小学生类似于GPU的计算单元,而对一些复杂的逻辑推理等问题,比如公式推导、科技文章写作等高度逻辑化的任务,交给小学生显然不合适,这时大学教授更适合,这里的大学教授就是CPU的计算单元了,大学教授当然能处理加减乘除的问题,单个教授计算加减乘除比单个小学生计算速度更快,但是成本显然高很多。

CPU与GPU分别具有独立的内存系统,见下图。CPU端也称为Host端,CPU内存称为Host(主机)内存;GPU端也成为Device(设备)端,其内存称为Device内存。一般情况下,如果我们要在GPU端进行计算,就需要把待处理的数据拷贝到到Device内存中,待数据处理完成之后,还需要把计算结果拷贝到Host端做进一步的处理,比如存储到硬盘中或者打印到显示器上。
在这里插入图片描述
在这里插入图片描述
GPU采用流式并行计算模式,可对每个数据进行独立的并行计算,所谓“对数据进行独立计算”,即,流内任意元素的计算不依赖于其它同类型数据,例如,计算一个顶点的世界位置坐标,不依赖于其他顶点的位置。而所谓“并行计算”是指“多个数据可以同时被使用,多个数据并行运算的时间和1个数据单独执行的时间是一样的”。

如何在GPU端分配与释放内存以及如何在CPU与GPU之间进行数据的拷贝?

int main(void) {
    size_t size = N * sizeof(int);
    int *h_a, *h_b; 
    int *d_a, *d_b, *d_c;
    h_a = (int *)malloc(size);
    h_b = (int *)malloc(size);
    ...
    cudaMalloc((void **)&d_a, size);
    cudaMalloc((void **)&d_b, size);
    cudaMalloc((void **)&d_c, size);

    cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
    vectoradd<<<Grid, block>>>(d_a, d_b, d_c, N);
    cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);

    cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
    free(h_a); free(h_b);
    return 0;
}

总共声明了两类不同的指针,int *h_a, *h_b;以及int *d_a, *d_b, *d_c;。前者是Host指针,指向host端内存;后者是Device指针,指向Device端内存。Device端的指针可以在主机端调用,但是不能在主机端解引用。主机端指针同样不能在设备端解引用。这里的设备端指针并不是位于设备端,而是指向设备端的内存。指针仍然是在主机端上,所以主机端可以使用这个指针,但是不能够解引用指向设备端内存的指针。

内存分配与释放

cudaMalloc(void**, size_t)
cudaMemset(void*, int, size_t) 
cudaFree(void*)

内存拷贝

cudaMemcpy ( void* dst, const void* src, size_t count, cudaMemcpyKind kind ) 

其中dst代表目的内存地址,src代表源内存地址,count代表需要拷贝的内存大小(bytes),kind代表数据拷贝的方向,必须是cudaMemcpyHostToHost, cudaMemcpyHostToDevice, cudaMemcpyDeviceToHost, cudaMemcpyDeviceToDevice以及cudaMemcpyDefault之一。可以看出该函数既可以实现在CPU与GPU内存之间的拷贝,也可以实现GPU内部之间的数据拷贝。需要注意的是cudaMemcpyDefault只能在支持统一虚拟地址(UVA)的系统上实现。在调用cudaMemcpy的时候,如果dst以及src的指针与拷贝的方向不一致时将会导致错误。相应的,采用 cudaMallocPitch()以及cudaMalloc3D()分配的内存,可以采用 cudaMemcpy2D()以及cudaMemcpy3D()来传输数据。
cudaMalloc是在设备端动态的分配内存,类似于CPU代码,我们也可以直接在设备端声明一个数据,这里需要用到__device__以及__constant__等标识符。下面的一段代码展示了不同访问device内存的方式:

__constant__ float constData[256];
float data[256];
cudaMemcpyToSymbol(constData, data, sizeof(data));
cudaMemcpyFromSymbol(data, constData, sizeof(data));

__device__ float devData;
float value = 3.14f;
cudaMemcpyToSymbol(devData, &value, sizeof(float));

__device__ float* devPointer;
float* ptr;
cudaMalloc(&ptr, 256 * sizeof(float));
cudaMemcpyToSymbol(devPointer, &ptr, sizeof(ptr));

cudaMemcpy是阻塞式的API,也就是CPU端代码在调用该API时,只有当该API完成拷贝之后,CPU才能继续处理后面的任务。这有一个好处就是保证了计算结果已经完全从GPU端拷贝到了CPU。同时CUDA也提供了非阻塞拷贝的API:cudaMemcpyAsync(), 非阻塞拷贝也称为异步拷贝,指的是该API在拷贝完成之前就返回,使得CPU可以继续处理后续的代码。异步拷贝API使得CPU与GPU之间的数据拷贝与CPU计算的并发称为可能。

GPU内存分类

在NVIDIA的GPU中,内存(GPU的内存)被分为了全局内存(Global memory)、本地内存(Local memory)、共享内存(Shared memory)、寄存器内存(Register memory)、常量内存(Constant memory)、纹理内存(Texture memory)六大类。这六类内存都是分布在在RAM存储芯片或者GPU芯片上,他们物理上所在的位置,决定了他们的速度、大小以及访问规则。
GPU结构图如下:
在这里插入图片描述
CUDA的内存模型,如下图所示。可以看到,每个线程有自己的私有本地内存(Local Memory),而每个线程块有包含共享内存(Shared Memory),可以被线程块中所有线程共享,其生命周期与线程块一致。此外,所有的线程都可以访问全局内存(Global Memory)。还可以访问一些只读内存块:常量内存(Constant Memory)和纹理内存(Texture Memory)。
在这里插入图片描述
一个kernel实际上会启动很多线程,这些线程是逻辑上并行的,但是在物理层却并不一定。这其实和CPU的多线程有类似之处,多线程如果没有多核支持,在物理层也是无法实现并行的。但是好在GPU存在很多CUDA核心,充分利用CUDA核心可以充分发挥GPU的并行计算能力。GPU硬件的一个核心组件是SM,前面已经说过,SM是英文名是 Streaming Multiprocessor,翻译过来就是流式多处理器。SM的核心组件包括CUDA核心,共享内存,寄存器等,SM可以并发地执行数百个线程,并发能力就取决于SM所拥有的资源数。当一个kernel被执行时,它的gird中的线程块被分配到SM上,一个线程块只能在一个SM上被调度。SM一般可以调度多个线程块,这要看SM本身的能力。那么有可能一个kernel的各个线程块被分配多个SM,所以grid只是逻辑层,而SM才是执行的物理层。SM采用的是SIMT (Single-Instruction, Multiple-Thread,单指令多线程)架构,基本的执行单元是线程束(wraps),线程束包含32个线程,这些线程同时执行相同的指令,但是每个线程都包含自己的指令地址计数器和寄存器状态,也有自己独立的执行路径。所以尽管线程束中的线程同时从同一程序地址执行,但是可能具有不同的行为,比如遇到了分支结构,一些线程可能进入这个分支,但是另外一些有可能不执行,它们只能死等,因为GPU规定线程束中所有线程在同一周期执行相同的指令,线程束分化会导致性能下降。当线程块被划分到某个SM上时,它将进一步划分为多个线程束,因为这才是SM的基本执行单元,但是一个SM同时并发的线程束数是有限的。这是因为资源限制,SM要为每个线程块分配共享内存,而也要为每个线程束中的线程分配独立的寄存器。所以SM的配置会影响其所支持的线程块和线程束并发数量。总之,就是网格和线程块只是逻辑划分,一个kernel的所有线程其实在物理层是不一定同时并发的。所以kernel的grid和block的配置不同,性能会出现差异,这点是要特别注意的。还有,由于SM的基本执行单元是包含32个线程的线程束,所以block大小一般要设置为32的倍数。
在这里插入图片描述
CUDA编程的逻辑层和物理层

寄存器内存(Register memory)
访问速度最快,但数量有限
使用:在__global__函数 ,或者___device__ 函数内,定义的普通变量,就是寄存器变量。

//kernel.cu
__global__ void register_test()
{
    int a = 1.0;
    double b = 2.0;
}

//main.cu
int main()
{
    int nBlock = 100;
    register_test <<<nBlock,128>>>();
    return 0;
}

共享内存(Shared memory)
(1)位置:设备内存。
(2)形式:关键字__shared__添加到变量声明中。如 __share __ double A[128]。
(3)目的:对于GPU上启动的每个线程块,CUDA C编译器都将创建该共享变量的一个副本。线程块中的每个线程都共享这块内存,但线程却无法看到也不能修改其他线程块的变量副本。这样使得一个线程块中的多个线程能够在计算上通信和协作。

优点:
1 缓存速度快 比全局内存 快2两个数量级
2 线程块内,所有线程可以读写。
3 生命周期与线程块同步
缺点:大小有限制

使用场合,如规约求和 : a = sum A[i]
如果不是频繁修改的变量,比如矢量加法。

C[i] = A[i] + B[i] 则没有必要将A,B进行缓存到shared memory 中。

//kernel.cu
__global__ void shared_test()
{
    __shared__ double A[128];
    int a = 1.0;
    double b = 2.0;
    int tid = threadIdx.x;
    A[tid] = a;
}

全局内存 (Global Memory)
通俗意义上的设备内存。
优点:
1空间最大(GB级别)
2.可以通过cudaMemcpy 等与Host端,进行交互。
3.生命周期比Kernel函数长
4.所有线程都能访问
缺点:访存最慢

//kernel.cu
__global__ void shared_test(int *B)
{
    double b = 2.0;
    int tid = threadIdx.x;
    int id = blockDim.x*128 + threadIdx.x;
    int a = B[id] ;
}

纹理内存
(1)位置:设备内存
(2)目的:能够减少对内存的请求并提供高效的内存带宽。是专门为那些在内存访问模式中存在大量空间局部性的图形应用程序设计,意味着一个线程读取的位置可能与邻近线程读取的位置“非常接近”。
(3)纹理变量(引用)必须声明为文件作用域内的全局变量。
在这里插入图片描述
优点,比普通的global memory 快
缺点:使用起来,需要四个步骤,麻烦一点
适用场景:比较大的只需要读取array,采用纹理方式访问,会实现加速

形式:分为一维纹理内存 和 二维纹理内存。
一维纹理内存
(1)用texture<类型>类型声明,如texture texIn。
(2)通过cudaBindTexture()绑定到纹理内存中。
(3)通过tex1Dfetch()来读取纹理内存中的数据。
(4)通过cudaUnbindTexture()取消绑定纹理内存。
二维纹理内存
(1)用texture<类型,数字>类型声明,如texture<float,2> texIn。
(2)通过cudaBindTexture2D()绑定到纹理内存中。
(3)通过tex2D()来读取纹理内存中的数据。
(4)通过cudaUnbindTexture()取消绑定纹理内存。

使用的四个步骤(以1维float数组为例子)

#include <iostream>
#include <time.h>
#include <assert.h>
#include <cuda_runtime.h>
#include "helper_cuda.h"
#include <iostream>
#include <ctime>
#include <stdio.h>

using namespace std;

texture<float, 1, cudaReadModeElementType> tex1D_load;

//第一步,声明纹理空间,全局变量
__global__ void kernel(float *d_out, int size)
{
    //textD_load 为全局变量,不在参数表中
    int index;
    index = blockIdx.x * blockDim.x + threadIdx.x;
    if (index < size)
    {
        d_out[index] = tex1Dfetch(tex1D_load, index); //第三步,抓取纹理内存的值
        //从纹理中抓取值
        printf("%f\n", d_out[index]);
    }
}

int main()
{
    int size = 120;
    size_t Size = size * sizeof(float);
    float *harray;
    float *d_in;
    float *d_out;
    harray = new float[size];
    checkCudaErrors(cudaMalloc((void **)&d_out, Size));
    checkCudaErrors(cudaMalloc((void **)&d_in, Size));
    //initial host memory
    for (int m = 0; m < 4; m++)
    {
        printf("m = %d\n", m);
        for (int loop = 0; loop < size; loop++)
        {
            harray[loop] = loop + m * 1000;
        }
        //拷贝到d_in中
        checkCudaErrors(cudaMemcpy(d_in, harray, Size, cudaMemcpyHostToDevice));
        //第二步,绑定纹理
        checkCudaErrors(cudaBindTexture(0, tex1D_load, d_in, Size));
        //0表示没有偏移
        int nBlocks = (Size - 1) / 128 + 1;
        kernel<<<nBlocks, 128>>>(d_out, size); //第三步
        cudaUnbindTexture(tex1D_load);         //第四,解纹理
        getLastCudaError("Kernel execution failed");
        checkCudaErrors(cudaDeviceSynchronize());
    }
    delete[] harray;
    cudaUnbindTexture(&tex1D_load);
    checkCudaErrors(cudaFree(d_in));
    checkCudaErrors(cudaFree(d_out));
    return 0;
}

常量内存
1. 位置:设备内存
2. 形式:关键字__constant__添加到变量声明中。如__constant__ float s[10];。
3. 目的:为了提升性能。常量内存采取了不同于标准全局内存的处理方式。在某些情况下,用常量内存替换全局内存能有效地减少内存带宽。
4. 特点:常量内存用于保存在核函数执行期间不会发生变化的数据。变量的访问限制为只读。NVIDIA硬件提供了64KB的常量内存。不再需要cudaMalloc()或者cudaFree(),而是在编译时,静态地分配空间
5. 要求:当我们需要拷贝数据到常量内存中应该使用cudaMemcpyToSymbol(),而cudaMemcpy()会复制到全局内存。
6. 性能提升的原因:
(1)对常量内存的单次读操作可以广播到其他的“邻近”线程。这将节约15次读取操作。(为什么是15,因为“邻近”指半个线程束,一个线程束包含32个线程的集合。)
(2) 常量内存的数据将缓存起来,因此对相同地址的连续读操作将不会产生额外的内存通信量。

固定内存
(1)位置:主机内存。
(2)概念:也称为页锁定内存或者不可分页内存,操作系统将不会对这块内存分页并交换到磁盘上,从而确保了该内存始终驻留在物理内存中。因此操作系统能够安全地使某个应用程序访问该内存的物理地址,因为这块内存将不会破坏或者重新定位。
(3)目的:提高访问速度。由于GPU知道主机内存的物理地址,因此可以通过“直接内存访问DMA(Direct Memory Access)技术来在GPU和主机之间复制数据。由于DMA在执行复制时无需CPU介入。因此DMA复制过程中使用固定内存是非常重要的。
(4)缺点:使用固定内存,将失去虚拟内存的所有功能;系统将更快的耗尽内存。
(5)建议:对cudaMemcpy()函数调用中的源内存或者目标内存,才使用固定内存,并且在不再需要使用它们时立即释放。
(6)形式:通过cudaHostAlloc()函数来分配;通过cudaFreeHost()释放。
(7)只能以异步方式对固定内存进行复制操作。

原子性
1. 概念:如果操作的执行过程不能分解为更小的部分,我们将满足这种条件限制的操作称为原子操作。
2. 形式:函数调用,如atomicAdd(addr,y)将生成一个原子的操作序列,这个操作序列包括读取地址addr处的值,将y增加到这个值,以及将结果保存回地址addr。
常用线程操作函数
1. 同步方法__syncthreads(),这个函数的调用,将确保线程块中的每个线程都执行完__syscthreads()前面的语句后,才会执行下一条语句。
使用事件来测量性能
1. 用途:为了测量GPU在某个任务上花费的时间。CUDA中的事件本质上是一个GPU时间戳。由于事件是直接在GPU上实现的。因此不适用于对同时包含设备代码和主机代码的混合代码设计。
2. 形式:首先创建一个事件,然后记录事件,再计算两个事件之差,最后销毁事件。

cudaEvent_t start, stop;
cudaEventCreate( &start );
cudaEventCreate( &stop );
cudaEventRecord( start, 0 );
//do something
cudaEventRecord( stop, 0 );
float   elapsedTime;
cudaEventElapsedTime( &elapsedTime,start, stop );
cudaEventDestroy( start );
cudaEventDestroy( stop );

总结表:
在这里插入图片描述

manjiaro(linux)查看cpu,gpu各种信息

lscpu //CPU信息
lsusb //usb设备
sudo ~/tegrastats //gpu使用情况

显卡服务器查看

nvidia-smi

在这里插入图片描述
训练数据的时候,想实时观察GPU的利用情况,因此需要添加一个定时输出的功能。这时候就需要用到 watch命令,来周期性地执行nvidia-smi命令。
参数 -n, 后面指定是每多少秒来执行一次命令。

watch -n 1 nvidia-smi  //设置每 1s 显示GPU使用情况:

在这里插入图片描述

参考博客:https://blog.csdn.net/Jerry_ICCAS/article/details/93378119

发布了32 篇原创文章 · 获赞 13 · 访问量 8327

猜你喜欢

转载自blog.csdn.net/qq_23858785/article/details/96828549
今日推荐