• [CUDA] CUDA编程入门


    CUDA编程入门

    Hello World

    首先一段程序写个hello world

    #include <stdio.h>
    __global__ void hello(){
        printf("Hello, threadIdx is:%d
    ",threadIdx.x);
    }
    
    int main(){
        hello<<<1,32>>>();
        cudaDeviceReset();
    }
    

    编译

    nvcc -o main main.cu
    

    运行

    ./main
    

    结果

    Kernels

    kernel在cuda中指的是一个函数,当一个kernel被调用的时候,gpu会同时启动很多个线程来执行这一个kernel,这样就实现了并行化;每个线程执行这一kernel将通过线程号来对应输入数据的下标,这样保证每个thread执行的kernel一样,但是处理的数据不一样。

    一个kernel在cuda中可以这么定义:

    // kernel定义
    __global__ void VecAdd(float* A,float* B, float C){
        int i = threadIdx.x;
        C[i] = A[i] + B[i]
    }
    
    // 调用
    VecAddd<<<1,N>>>(A,B,C);
    

    上面代码中调用的时候通过指定<<<1,N>>>前者1表明使用1个block,每个block有N个threads,这样在函数调用内部通过取内置变量threadIdx来得到thread的索引,以此对应数组下标,实现并行化。

    __global__前缀表示在GPU上执行,其他对应的前缀有如下特点:

    1. __global__:在GPU上执行,可以在CPU上被调用,也可以在GPU上被调用
    2. __device__:在GPU上执行,只能在GPU上被调用
    3. __host__:在CPU上执行,只能在CPU上被调用

    实际上,可以先缕一缕GPU中内存的一些关系:

    可以看到,GPU的整个内存被划分为了多个grid,每个grid有很多block,而每个block都共享同样的内存,每个block的多条thread针对共享的内存并行执行不同的操作。

    上面的例子中只是针对thredIdx是一维的情况,实际上,为了方便,我们可以直接将threadIdx变成多维的,以方便我们对矩阵或者更高维的数据的处理,这跟C语言里的二维数组等概念是一样的,就是用x和y分别代表一个维度的下标。

    __global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]){
        int x = threadIdx.x;
        int y = threadIdx.y;
        C[x][y] = A[x][y] + B[x][y];
    }
    
    int main(){
        int num_blocks = 1;
        dim3 threads_per_block(N,N);
        MatAdd<<<num_blocks,threads_per_block>>>(A,B,C);
    }
    
    

    这个例子是相对很容易理解的了。需要知道的是,每个block的threads数目是有最大限制的,不能超过一定的值,书上说现在的GPU每个block最多是1024个thread。所以上面的例子其实并不能处理很大的矩阵,那么要处理很大的矩阵的时候该怎么办呢?

    可以用上block:

    ___global___ void MatAdd(float A[N][N], float B[N][N], float C[N][N]){
        int i = blockIdx.x * blockDim.x + threadIdx.x;
        int j = blockIdx.y * blockDim.y + threadIdx.y;
        C[i][j] = A[i][j] + B[i][j];
    }
    int main(){
        dim3 threads_per_block(16,16);
        dim3 num_blocks(N/num_blocks.x,N/num_blocks.y);
        MatAdd(A,B,C);
    }
    

    这个例子充分利用了block数量,这样每个block是16*16也就是256个threads,假设N = 32,那么num_blocks = (2,2)也就是用了4个block。上面blockIdx.x取值是[0,1],blockDim.x取值是16,threadIdx.x取值是[0,15].

    假设每个block最多可以用1024个thread,那么其实可以处理$N_{max} = 32 * sqrt{num_blocks} $阶矩阵。

    那这里就有个疑问了,不是说一个block一块内存吗,不同block之间不share内存,那这个代码感觉所有block都share同一块内存的样子呀?其实这里跟share不share没任何关系。。每个thread都在独立计算一条语句,大家根本没有说有那种你先我后的协作关系,所以谈不上share,所以大家都是independent的,从global memory里读入,然后写到global memory。

    Compute Capability

    一般用x.y来表示一款GPU的计算能力,x相同的表示架构相同,y只是在这个架构上做了一些increasement。

    现在GPU与计算能力的x之间有下图这样的对应:

    7 Volta
    6 Pascal
    5 Maxwell
    3 Kepler
    2 Fermi
    1 Tesla

    这里注意不要与cuda版本号搞混了,计算能力描述的是gpu,而非软件版本。

    Memory Allocation

    这节涉及之前省略的,之前的代码没有考虑A、B、C的内存申请,这一节讨论的就是如何把host的内存(cpu data)转到device里(gpu data)。

    如果要申请一块连续的GPU内存空间,直接可以通过调用cudaMalloc()来实现,调用cudaFree()释放空间,那么把host上内存copy到gpu上是通过调用cudaMemcpy()来实现。

    __global__ void VecAdd(float* A, float* B, float* C){
        int i = blockIdx.x * blockDim.x + threadIdx.x;
        // 根据下面的分析,i是可能大于等于N的,当N>threadsPerBlock且为threadsPerBlock的非整数倍的时候,这时候会引入多一个block来运算,其中多余的threads不应该参与运算,所以下面if需要判断。
        if (i < N){
            C[i] = A[i] + B[i];
        }
    
    }
    
    int main(){
        int N = 32;
        size_t size = N * sizeof(float);
        float* h_A = (float*)malloc(size);
        float* h_B = (float*)malloc(size);
        float* h_C = (float*)malloc(size);
    
        float* d_A;
        float* d_B;
        float* d_C;
        
        cudaMalloc(&d_A,size);
        cudaMalloc(&d_B,size);
        cudaMalloc(&d_C,size);
        
        cudaMemcpy(d_A, h_A,size,cudaMemcpyHostToDevice);
        cudaMemcpy(d_B, h_B,size,cudaMemcpyHostToDevice);
    
        int threadsPerBlock = 256;
        // 由于threadsPerBlock通常是定的,所以我们需要调整的一般是Blocks的数量
        // 考虑到如果N < threadsPerBlock,那么N/threadsPerBlock就是0,但这是是少需要一个block
        // 如果N = threadsPerBlock,这时候如果(N + threadsPerBlock)/threadsPerBlock就是2,但其实这时候只需要一个block
        // 综上,应该是下式才满足条件
        int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    
        VecAdd<<<blocksPerGrid,threadsPerBlock>>>(d_A,d_B,d_C,N);
        
        cudaMemcpy(h_C,d_C,cudaMemcpyDeviceToHost);
        
        cudaFree(d_A);
        cudaFree(d_B);
        cudaFree(d_C);
    }
    

    cudaMalloc ( void** devPtr, size_t size )

    Parameters
    • devPtr
      • Pointer to allocated device memory
    • size
      • Requested allocation size in bytes

    第一个参数是地址的指针,显然cudaMalloc只能分配线性内存,线性内存访问起来是相当块的。在传统C语言里,我们如果要取用malloc申请二维数组,可能先申请一些一维数组用来存储首地址指针,每个首地址再去指向一个申请的一维数组,实现二维数组,在CUDA里可以通过cudaMallocPitch来申请二维数组,得到的地址仍然是线性的,因为访问块。

    cudaMallocPitch ( void** devPtr, size_t* pitch, size_t width, size_t height )

    Parameters
    • devPtr

      • Pointer to allocated pitched device memory
    • pitch

      • Pitch for allocation
    • width

      • Requested pitched allocation width (in bytes)
    • height

      • Requested pitched allocation height

    pitch参数表示的是“宽度”,可以理解为一行有多少个元素乘以sizeof(float),是返回值。

    书上的一个例子为:

    __global__ Mykernel(float* devPtr,size_t pitch, int width, int height){
        for(int r = 0;r < height; ++r){
            float* row = (float*)((char*)devPtr + r * pitch); // 转char* 是因为原来的devPtr是float*的,加法运算会考虑stride。
            for(int c=0;c<width;++c){
                float val = row[c];
            }
        }
    }
    
    int width = 64,height = 64;
    float* devPtr;
    size_t pitch; // sizeof(float) * width
    cudaMallocPitch(&devPtr,&pitch,width * sizeof(float),height); // height不用乘sizeof(float)是因为第一维的指针是void*
    
    Mykernel<<<100,512>>>(devPtr,pitch,width,height);
    

    Shared Memory

    本节通过一个矩阵相乘的例子,来说明通过block内shared memory来减少从global memory读取和写入数据的次数,从而加速矩阵相乘的速度。

    首先是没有shared memory的代码实现:

    typedef struct{
        int width;
        int height;
        float* elements;
    } Matrix;
    
    #define BLOCK_SIZE 16
    
    __global__ void MatMulKernel(const Matrix, const Matrix, Matrix);
    
    void MatMul(const Matrix A, const Matrix B, Matrix C){
        Matrix d_A;
        d_A.width = A.width;
        d_A.height = A.height;
        size_t size = d_A.width * d_A.height * sizeof(float);
        cudaMalloc(&d_A.elements,size);
        cudaMemcpy(d_A.elements,A.elements,size,cudaMemcpyHostToDevice);
    
        Matrix d_B;
        d_B.width = B.width;
        d_B.height = B.height;
        size_t size = d_B.width * d_B.height * sizeof(float);
        cudaMalloc(&d_B.elements,size);
        cudaMemcpy(d_B.elements,B.elements,size,cudaMemcpyHostToDevice);
    
        Matrix d_C;
        d_C.width = C.width;
        d_C.height = C.height;
        size_t size = d_C.width * d_C.height * sizeof(float);
        cudaMalloc(&d_C.elements,size);
        
        dim3 dimBlock(BLOCK_SIZE,BLOCK_SIZE);
        // 列索引和行索引划分
        dim3 dimGrid(B.width / dimBlock.x,A.height / dimBlock.y);
        MatMulKernel<<<dimBlock,dimGrid>>>(d_A,d_B,d_C);
    
        cudaMemcpy(C.elements,d_C.elements,size,cudaMemcpyDeviceToHost);
        cudaFree(d_A.elements);
        cudaFree(d_B.elements);
        cudaFree(d_C.elements);
    }
    
    __global__ void MatMulKernel(Matrix A, Matrix B, Matrix C){
        float Cvalue = 0;
        // 这里求对应A矩阵的第row行以及B矩阵的第j列
        int row = blockIdx.y * blockDim.y + threadIdx.y;
        int col = blockIdx.x * blockDim.x + threadIdx.x;
        for(int i=0;i<A.width;i++){
            Cvalue += A.elements[row * A.width + i] * B.elements[i * B.width + col];
        }
        C.elements[row * C.width + col] = Cvalue;
    }
    

    代码很好理解,每个thread负责通过一个循环来计算输出矩阵C中的一个数值,需要注意的是,C矩阵中的每个数值计算,都需要一个thread从global memory中读取数据,计算完成,然后写回。下图表达的就是这样的一个计算实例:

    所以总的读取和写入次数是非常频繁的,为了减少访问global memory的次数,可以对C矩阵分块求取,C矩阵中的每个分块矩阵由一个block负责计算,这block一次将数据读入block内的共享内存,然后thread在共享内存上计算,最后写入到global memory,这大大减少了访问global memory的次数,加速了计算。

    // 加的这个stride就是为了表示原矩阵width,这里的width和height可以是子矩阵的width和height
    // elements是子矩阵的首地址
    // M(row,col) = *(M.elements + row * M.stride + col)
    typedef struct{
        int width;
        int height;
        int stride;
        float* elements;
    } Matrix;
    #define BLOCK_SIZE 16
    __device__ void SetElement(Matrix A, int row, int col, float value){
        // 虽然A是一个局部变量,但是A里面的elements指向的东西没被深拷贝
        A.elements[row * A.stride + col] = value; // 乘以A的stride是因为A跨行的stride和原矩阵一致
    }
    __device__ float GetElement(Matrix A,int row, int col){
        return A.elements[row * A.stride + col];
    }
    
    __device__ Matrix getSubMatrix(Matrix A,int row, int col){
        Matrix Asub;
        Asub.width = BLOCK_SIZE;
        Asub.height = BLOCK_SIZE;
        Asub.stride = A.stride;
        Asub.elements = A.elements + (BLOCK_SIZE * row) * stride + (BLOCK_SIZE*col) // 相当于乘以BLOCK_SIZE
        return Asub;
    }
    
    
    __global__ void MatMulKernel(const Matrix, const Matrix, Matrix);
    
    void MatMul(const Matrix A, const Matrix B, Matrix C){
        Matrix d_A;
        d_A.width = A.width;
        d_A.height = A.height;
        size_t size = d_A.width * d_A.height * sizeof(float);
        cudaMalloc(&d_A.elements,size);
        cudaMemcpy(d_A.elements,A.elements,size,cudaMemcpyHostToDevice);
    
        Matrix d_B;
        d_B.width = B.width;
        d_B.height = B.height;
        size_t size = d_B.width * d_B.height * sizeof(float);
        cudaMalloc(&d_B.elements,size);
        cudaMemcpy(d_B.elements,B.elements,size,cudaMemcpyHostToDevice);
    
        Matrix d_C;
        d_C.width = C.width;
        d_C.height = C.height;
        size_t size = d_C.width * d_C.height * sizeof(float);
        cudaMalloc(&d_C.elements,size);
        
        dim3 dimBlock(BLOCK_SIZE,BLOCK_SIZE);
        // 列索引和行索引划分
        dim3 dimGrid(B.width / dimBlock.x,A.height / dimBlock.y);
        MatMulKernel<<<dimBlock,dimGrid>>>(d_A,d_B,d_C);
    
        cudaMemcpy(C.elements,d_C.elements,size,cudaMemcpyDeviceToHost);
        cudaFree(d_A.elements);
        cudaFree(d_B.elements);
        cudaFree(d_C.elements);
    }
    
    __global__ void MatMulKernel(Matrix A, Matrix B, Matrix C){
        int block_row = blockIdx.x;
        int block_rol = blockIdx.y;
        Matrix Csub = GetSubMatrix(C,block_row,block_col);
        float Cvalue = 0;
        int row = threadIdx.x;
        int col = threadIdx.y;
    
        for(int i=0;i<(A.width / BLOCK_SIZE);++i){
            // 这里不是直接一行算,A矩阵和B矩阵也是按block来,最后是Cvalue求和不影响结果
            Matrix Asub = GetSubMatrix(A,block_row,i);
            Matrix Bsub = GetSubMatrix(B,i,block_rol);
            // 直接生命的对象 局部生命周期
    
            __shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
            __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];
            
            As[row][col] = GetElement(Asub,row,col);
            Bs[row][col] = GetElement(Bsub,row,col);
            // 保证整个子矩阵都被读入
            // 也就是一个block的所有thread都执行到这里
            __syncthreads();
    
            for(int e=0; e<BLOCK_SIZE;++e){
                Cvalue += As[row][e] * Bs[e][col];
            }
            // 保证这个block算完,然后下一次训练读入下一轮的sub matrix
            __syncthreads();
        }
        SetElement(Csub,row,col,Cvalue)
    }
    

    假设A矩阵是m行n列的,B矩阵是p行q列的,如果用前一种方法,A和B矩阵从global memory中被读取的次数是np次,后一种方法是(n/block_size) * (p/block_size)次。(书上那种说法应该指的是同一行、列数据被读取的次数,跟我这里不同)。

    大致可用下图表示:

    Pinned Memory

    Host端的内存分为页内存(Pageable Memory)和锁存(Pinned Memory),前者在内存不足时会与虚拟内存交换,把数据放在虚拟内存中,后者内存是固定的。

    而内存拷贝是一个非常耗时的操作,由于GPU知道内存的物理地址,因此就可以使用DMA技术来在GPU和CPU之间复制数据.当使用可分页的内存进行复制时(使用malloc),CUDA驱动程序仍会通过dram把数据传给GPU,这时复制操作会执行两遍,第一遍从可分页内存复制一块到临时的页锁定内存,第二遍是再从这个页锁定内存复制到GPU上.当从可分页内存中执行复制时,复制速度将受限制于PCIE总线的传输速度和系统前段速度相对较低的一方.在某些系统中,这些总线在带宽上有着巨大的差异,因此当在GPU和主机之间复制数据时,这种差异会使页锁定主机内存比标准可分页的性能要高大约2倍.即使PCIE的速度于前端总线的速度相等,由于可分页内训需要更多一次的CPU参与复制操作,也会带来额外的开销.

    所以在内存足够的时候,可以使用pinned memory来加速数据读取。

  • 相关阅读:
    对坐标点的离散化
    线段树-离散化处理点
    树状数组
    线段树
    dfs
    vector
    go 参数传递的是值还是引用 (转)
    go 数组指针 指针数组
    go 协程
    go 接口实现
  • 原文地址:https://www.cnblogs.com/aoru45/p/12650861.html
Copyright © 2020-2023  润新知