zoukankan      html  css  js  c++  java
  • 并行计算基础(2)

    一、CPU和GPU交互

    1.各自有自己的物理内存空间,CPU的是内存,GPU的是显存

    2.通过PCI-E总线互连(8GB/S~16GB/S)

    3.交互开销较大

    GPU各存储访存速度:

    Register寄存器,最快

    Shared Memory,共享存储,很快

    Local Memory,本地存储,在显存中,有缓存,相对较慢

    Global Memory,全局存储,在显存中,有缓存,相对较慢

    Constant Memory,在显存中,多级缓存,1-100时钟周期,比较快

    Texture Memory,在显存中,多级缓存,1-100时钟周期,比较快

    Instruction Memory,不可见的,在显存中,有缓存

    二、GPU线程组织模型

     线程组成Block,Block组成Grid。

     Warp是几个线程的组合,有一定特殊的规律,用于内部管理。

    线程组织架构说明:

    1.一个Kernel就是一个要运行的程序,里面有大量的线程。Kernel启动一个Grid,里面有若干个Blocks,由用户设定。Grid可以理解为一个公司。

    2.一个Block中包含多个线程,一个Block内部的线程共享Shared Memory,可以同步“_syncthreads()”。Block可以理解为一个部门。

    3.线程和线程块具有唯一的标识。

    程序对于GPU也有一定的映射关系:

    其中,一个线程对应一个CUDA core或ALU,一个Block对应一个SM或SMX,一个Grid对应多个SM,最大为整个设备。

    GPU内存和线程的关系:

    1.一个线程有自己的存储器,叫做Local Memory,是私有的,只能自己访问。例如私人的办工作,电脑等资源。

    2.每个Block,有内部线程可共享的Shared Memory,相当于部门中的打印机等共享资源。

    3.每个Grid(Kernal)之间有共享的Global Memory,也就是GPU设备的全局存储。相当于多个公司都可以访问的大楼。

    4.主机端的存储器(内存)可以和不同的GPU设备的内存(显存)相互拷贝数据。

    如下图所示:

    1.线程运算时与寄存器交互最快。

    2.线程读取Local Memory时,由于该存储位于外部显存,所以速度相当较慢。

    3.一个Block中共享Shared Memory。

    4.各个Block中的线程都可以访问Global Memory。

    5.Constant和Texture对于线程都是只读的存储。

    6.Constant和Texture可以由主机端来读写。

    三、CUDA编程模式

    CUDA编程语言实际上是扩展的C语言(Extended C)

    CUDA提供了许多特定的关键词。例如__device__,__global__,__host__等。

    CUDA函数声明:

    __device__ float DeviceFunc();
    __global__ void KernelFunc();
    __host__ float HostFunc();

    1.由__device__修饰的函数声明表示该函数的执行位置是在GPU设备上,需要由其他GPU上的函数来调用。

    2.由__global__修饰的函数是kernel函数,也是入口函数,在CPU上调用,在GPU上执行,必须返回void。

    3.__host__修饰的函数是在主机端调用,也在主机端运行。

    4.__device__和__host__可以同时作用于一个函数,说明该函数的操作在CPU和GPU上是一样的。

    Kernel:

      数据并行处理函数。

      通过调用Kernel函数在设备端创建轻量级线程,线程由硬件负责创建并调度。

      Kernel函数是在CPU上调用,然后再GPU上执行,是一个入口函数。

    // 定义一个Kernel函数用__global__修饰
    __global__ void VecAdd(float * A, float *B, float *C) {
        int i = threadIdx.x;
        C[i] = A[i] + B[i];
    }
    
    int main() {
        //.....需要将A B都拷贝到显存
        //.....在显存中分配C的空间
        // 使用N个线程来计算
        VecAdd<<<1, N>>> (A, B, C);
    
        return 0;
    }

    线程层次Thread Hierarchies:

    使用一个Block来处理:

    __global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) {
        // 线程有N*N个,xy代表线程索引
        int i = threadIdx.x;
        int j = threadIdx.y;
        C[i][j] = A[i][j] + B[i][j];
    }
    
    int main() {
        // 使用一个Block
        int numBlocks = 1;
        // 每个Block有N*N个线程
        dim3 threadPerBlock(N, N);
        // 这里使用一个Block,每个Block有N*N个线程
        MatAdd <<<numBlocks, threadPerBlock>>> (A, B, C);
    
        return 0;
    }

    上述代码中,只使用一个Block(一个部门),该Block中有N*N个线程(人员)。这个Block是一个2D的Block。

    Block中的线程:

    在G80和GT200显卡中,每个Block最多512个线程,而Fermi架构的GPU每个Block可以有1024个线程,可以查阅相关GPU手册。

    每个Block相当于一个SM,即核心。所以该Block中的线程都是工作在相同的处理器核心中的。他们共享所在核心的Shared Memory。

    使用多个Block处理:

    __global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) {
        // 遍历每个Block的所有元素,并分别执行加法
        int i = blockIdx.x * blockDim.x + threadIdx.x;
        int j = blockIdx.y * blockDim.y + threadIdx.y;
        if (i < N && j < N) {
            C[i][j] = A[i][j] + B[i][j];
        }
    }
    
    int main() {
        // 每个Block有16*16个线程
        dim3 threadPerBlock(16, 16);
        // 使用需要计算矩阵的尺寸来计算需要多少个Block
        dim3 numBlocks(N / threadPerBlock.x, N / threadPerBlock.y);
        // 这里使用一个Block,每个Block有N*N个线程
        MatAdd <<<numBlocks, threadPerBlock>>> (A, B, C);
    
        return 0;
    }

    Block与GPU核心(SM)数量关系:

    当GPU只有2个SM(核心)时,程序有需要8个Block,则需要通过2个核心4次运算才能完成。

    如果是4个核心,则需要2次运算才能完成。

    四、数据传输

    使用cudaMalloc在device上申请内存空间:

    // 该指针用于存放device上分配空间的首地址
    float * Md = 0;
    // 申请设备内存大小为size
    int size = 16 * 16 * sizeof(float);
    // 这里必须传入&Md,即Md指针的地址。
    // 因为cudaMalloc会将分配好的设备内存首地址赋值给Md,这个Md只能在Device上使用,不能直接在CPU程序中赋值等
    cudaMalloc((void **)&Md, size);
    // 释放Md指向的设备内存空间
    cudaFree(Md);

    内存传输:

      Host to Host

      Host to Device

      Device to Host

      Device to Device

    对应一下四种操作:

    // 申请设备内存大小为size
    int size = 16 * 16 * sizeof(float);
    
    // M指向CPU上的空间
    float * M = (float *)malloc(size);
    float * M2 = (float *)malloc(size);
    // Md指向GPU上的空间
    float * Md = 0;
    float * Md2 = 0;
    cudaMalloc((void **)&Md, size);
    cudaMalloc((void **)&Md2, size);
    
    // 从主机端内存中拷贝数据到Device的Global Memory中
    cudaMemcpy(Md, M, size, cudaMemcpyHostToDevice);
    // 从设备端拷贝数据到主机端
    cudaMemcpy(M, Md, size, cudaMemcpyDeviceToHost);
    // 从主机端数据拷贝到主机端另一个空间,相当于memcpy
    cudaMemcpy(M, M2, size, cudaMemcpyHostToHost);
    // 从设备端拷贝数据到设备端另一个空间
    cudaMemcpy(Md2, Md, size, cudaMemcpyDeviceToDevice);

    五、矩阵乘法示例

    // Md,Nd,Pd都是Width*Width的方阵,使用的Block中线程数也是W*W
    __global__ void MatMulKernel(float * Md, float * Nd, float * Pd, int Width) {
        // 横坐标为tx的列索引
        int tx = threadIdx.x;
        // 纵坐标为ty的行索引
        int ty = threadIdx.y;
        
        float Pvalue = 0;
        for (int k = 0;k < Width;++k) {
            // 处于tx的一行
            float Mdelement = Md[ty * Width + k];
            // 处于ty的一列
            float Ndelement = Nd[k * Width + tx];
            // Width元素做累加,得到坐标ty,tx的值
            Pvalue += Mdelement * Ndelement;
        }
        // 将计算得到的ty,tx的值写入相应的位置
        Pd[ty * Width + tx] = Pvalue;
    }

    六、GPU上函数需要注意的问题

    由于GPU特殊的工作情况和结构,在__Global__和__device__函数中,注意以下几点:

    1.尽量少用递归(不鼓励)

    2.不要使用静态变量

    3.少用malloc(允许但不鼓励,因为并行的使用malloc,空间很快耗光)

    4.小心通过指针实现的函数调用(注意指针时CPU端的还是GPU端的)

    七、CUDA数据类型

    矢量数据类型(同时适用于host和device代码):

     通过函数make_<type name>构造:

    int2 i2 = make_int2(1, 2);
    float4 f4 = make_float4(1.0f, 2.0f, 3.0f, 4.0f);
    cout << i2.x << i2.y << endl;
    cout <<  f4.x << f4.y << f4.z << f4.w << endl;

    八、CUDA支持的部分函数

    部分函数列表:

    面向Device端,更快,精度降低:

     九、线程同步

    块内线程可以同步(Block内):

      调用__syncthreads 创建一个barrier栅栏

      每个线程在调用点等待块内所有线程执行到这个地方,然后再继续执行后续指令

    Md[i] = Hd[j];
    __syncthreads();
    func(Md[i], Md[i + 1]);

    如上述代码所示,func中同时需要Md[i]和Md[i+1],当Md[i]准备好时,Md[i+1]不一定准备好了,所以需要在前面等待Md[i+1]准备好后,再继续执行func函数。

    __syncthreads会导致线程的暂停,破坏了线程执行的独立性,并可能由于线程同步的位置不同(条件分支中使用同步)导致同步死锁。所以在使用同步时一定要小心。

     十、线程调度

    以G80显卡为例:

    G80包含以下:

    1.有16个核,也就是SM(8个绿色方框为一个SM)

    2.每个SM有8个SP,也就是CUDA core或ALU(1个绿色方框)

    3.每个SM最多可驻扎768个线程,128 X 6 = 768,每个SM可以保存6个上下文(蓝色部分)

    4.总共可以同时驻扎12288个线程

    5.但是由于只有128个CUDA core,同时也就只能执行128个线程

    对于一个GPU设备来说,最大处理的线程量主要和CUDA core总量以及每个SM的上下文数量有关。但同时执行的线程数只与CUDA core数一致。

    Warp:

    针对Block中的线程,例如有64个线程(CUDA core),编号是连续的0-63。

    假设一个Warp是32个线程组成(Warp的线程数和Block的线程数一般呈倍数关系,warpSize),则该Block中就有2个Warp,都运行在同一个SM上。第一个Warp线程编号为0-31,第二个Warp的线程编号为32-63。

    Warp是线程调度的最小单位。

    Warp的线程是天生同步的,也就是说他们必须是执行相同的指令流,当遇到分支可能导致执行的程序不同时(例如if else)则会出现串行的可能:

    可能出现最差性能,就是1/N的性能。

    例子:

    1.如果一个SM分配了3个Block,其中每个Block含256个线程,那么总共有24个Warp(每个Warp 32个线程)。

    2.GT200的一个SM最多可以驻扎1024个线程,那相当于1024/32=32个Warp。

    3.假设每个Warp有32个线程,但每个SM只有8个SPs,如何分配?需要将一个Warp分成4份,然后在一个SM上轮换执行4次。流程如下:

      指令已经预备

      第一个周期8个线程进入SPs

      在第二、三、四周期各进入8个线程

      因此,分发一个Warp需要4个周期

    4.对于目前的GPU来说,SM中所含的SP数一般都大于Warp含线程数量,所以以上分发流程一般不会再出现。

    十一、内存模型

    寄存器:

    假设每个SM有8K个寄存器,有768个线程。则每个线程可以分到10个寄存器。

    当超出限制时,则将因为Block的减少而减少。

    例如,当一个线程需要用到11个寄存器,一个Block含256个线程。

    本来如果每个线程使用寄存器不超出限制的时候,这个SM可以容纳3个Block(一个Block内的线程只能在同一个SM上执行),也就是刚好768个线程。

    但由于寄存器超出限制,这个SM就只能容纳2个Block,即512个线程。所以就造成了资源的浪费。剩下未分配的SP也就只能闲着。

    共享存储:

    和寄存器类似原理类似。

    假设每个SM最多8个Block,一共有16KB共享存储器。如果一个Block需要大于2K的共享存储器,则这个SM就不能容纳8个Block,同样造成资源浪费。

    全局存储(显存):

    访存延时(100个周期),访存较慢,片外存储

    Host主机可读写

    GT200 GPU访存带宽150GB/s,容量4GB,新的显卡的访存带宽已达到300-500GB/s,容量达到8-32GB

    位于不同存储的变量定义:

     其中register和local存储我们不能操作。

    __shared__定义存放在共享存储中的变量,这个变量只能是Block内部线程共享。

    使用__device__关键字来定义全局存储(显存)中的变量。

    __constant__用来定义常量(例如PI),存放在constant Memory中的。

  • 相关阅读:
    (引)spring学习笔记1.什么是控制反转
    Arduino 各种模块篇 步进电机 step motor 舵机 servo 直流电机 总复习
    Raspberry Pi Wireless Adaptor
    Pyramid 使用总结1
    Arduino 各种模块篇 人体红外感应模块 proximity sensor
    Pyramid 使用总结2
    Webcam Streaming Desktop Recording on Linux for ubuntu or its destros
    Arduino 各种模块篇 步进电机 step motor( 不用库,不用shield, 纯)
    Arduino 各种模块篇 motor shield 电机扩展板(舵机、直流电机、步进电机party)
    转载 stepper motors
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/11431096.html
Copyright © 2011-2022 走看看