2022CUDA夏季训练营Day3实践

举报
张辉 发表于 2022/07/12 14:26:26 2022/07/12
【摘要】 CUDA

2022CUDA夏季训练营Day1实践 https://bbs.huaweicloud.cn/blogs/364478

2022CUDA夏季训练营Day2实践 https://bbs.huaweicloud.cn/blogs/364479

今天是第三天,主题是Event,Memory和Shared Memory。

(一)Event

我们先来看第一个——Event。

Event是CUDA中的事件,用于分析、检测CUDA程序中的错误。

一般我们会定义一个宏:

#pragma once
#include <stdio.h>

#define CHECK(call)                                   \
do                                                    \
{                                                     \
    const cudaError_t error_code = call;              \
    if (error_code != cudaSuccess)                    \
    {                                                 \
        printf("CUDA Error:\n");                      \
        printf("    File:       %s\n", __FILE__);     \
        printf("    Line:       %d\n", __LINE__);     \
        printf("    Error code: %d\n", error_code);   \
        printf("    Error text: %s\n",                \
            cudaGetErrorString(error_code));          \
        exit(1);                                      \
    }                                                 \
} while (0)

并在适当的位置使用这个宏来打印CUDA的错误日志。

注:#pragma once, 不要放在源代码文件里,这个一般只放在头文件里的。(防止头文件被引入多次)

具体的调用过程如下:

具体的顺序如下:

(1)声明Event(这里以计算核函数运行时间前后的start Event和stop Event为例)

cudaEvent_t start, stop;

(2)创建Event

CHECK(cudaEventCreate(&start));
CHECK(cudaEventCreate(&stop));

(3)添加Event(在合适的地方)

cudaEventRecord(start);
cudaEventRecord(stop);

(4)等待Event完成

(a)非堵塞方式——可以用于一些不需要等待的处理

cudaEventQuery(start);

(b)堵塞方式——可以用于执行核函数后等待核函数执行完毕后的处理

cudaEventSynchronize(stop);

(5)计算两个Event间隔时间

CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));

(6)销毁Event

CHECK(cudaEventDestroy(start));
CHECK(cudaEventDestroy(stop));

完整的代码如下:

#pragma once
#include <stdio.h>

#define CHECK(call)                                   \
do                                                    \
{                                                     \
    const cudaError_t error_code = call;              \
    if (error_code != cudaSuccess)                    \
    {                                                 \
        printf("CUDA Error:\n");                      \
        printf("    File:       %s\n", __FILE__);     \
        printf("    Line:       %d\n", __LINE__);     \
        printf("    Error code: %d\n", error_code);   \
        printf("    Error text: %s\n",                \
            cudaGetErrorString(error_code));          \
        exit(1);                                      \
    }                                                 \
} while (0)

#include <stdio.h>
#include <math.h>
#include "error.cuh"

#define BLOCK_SIZE 32

__global__ void gpu_matrix_mult(int *a,int *b, int *c, int m, int n, int k)
{ 
    int row = blockIdx.y * blockDim.y + threadIdx.y; 
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    int sum = 0;
    if( col < k && row < m) 
    {
        for(int i = 0; i < n; i++) 
        {
            sum += a[row * n + i] * b[i * k + col];
        }
        c[row * k + col] = sum;
    }
} 

void cpu_matrix_mult(int *h_a, int *h_b, int *h_result, int m, int n, int k) {
    for (int i = 0; i < m; ++i) 
    {
        for (int j = 0; j < k; ++j) 
        {
            int tmp = 0.0;
            for (int h = 0; h < n; ++h) 
            {
                tmp += h_a[i * n + h] * h_b[h * k + j];
            }
            h_result[i * k + j] = tmp;
        }
    }
}

int main(int argc, char const *argv[])
{
    int m=100;
    int n=100;
    int k=100;
    
    //声明Event
    cudaEvent_t start, stop, stop2, stop3 , stop4 ;
    
    //创建Event
    CHECK(cudaEventCreate(&start));
    CHECK(cudaEventCreate(&stop));
    CHECK(cudaEventCreate(&stop2));

    int *h_a, *h_b, *h_c, *h_cc;
    CHECK(cudaMallocHost((void **) &h_a, sizeof(int)*m*n));
    CHECK(cudaMallocHost((void **) &h_b, sizeof(int)*n*k));
    CHECK(cudaMallocHost((void **) &h_c, sizeof(int)*m*k));
    CHECK(cudaMallocHost((void **) &h_cc, sizeof(int)*m*k));

    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            h_a[i * n + j] = rand() % 1024;
        }
    }

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < k; ++j) {
            h_b[i * k + j] = rand() % 1024;
        }
    }

    int *d_a, *d_b, *d_c;
    CHECK(cudaMalloc((void **) &d_a, sizeof(int)*m*n));
    CHECK(cudaMalloc((void **) &d_b, sizeof(int)*n*k));
    CHECK(cudaMalloc((void **) &d_c, sizeof(int)*m*k));

    // copy matrix A and B from host to device memory
    CHECK(cudaMemcpy(d_a, h_a, sizeof(int)*m*n, cudaMemcpyHostToDevice));
    CHECK(cudaMemcpy(d_b, h_b, sizeof(int)*n*k, cudaMemcpyHostToDevice));

    unsigned int grid_rows = (m + BLOCK_SIZE - 1) / BLOCK_SIZE;
    unsigned int grid_cols = (k + BLOCK_SIZE - 1) / BLOCK_SIZE;
    dim3 dimGrid(grid_cols, grid_rows);
    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
   
    //开始start Event
    cudaEventRecord(start);
    //非阻塞模式
    cudaEventQuery(start);

    //gpu_matrix_mult<<<dimGrid, dimBlock>>>(d_a, d_b, d_c, m, n, k);   
    
    gpu_matrix_mult_shared<<<dimGrid, dimBlock>>>(d_a, d_b, d_c, m, n, k);  

    //开始stop Event
    cudaEventRecord(stop);
    //由于要等待核函数执行完毕,所以选择阻塞模式
    cudaEventSynchronize(stop);
    
    //计算时间 stop-start
    float elapsed_time;
    CHECK(cudaEventElapsedTime(&elapsed_time, start, stop));
    printf("start-》stop:Time = %g ms.\n", elapsed_time);

    
    cudaMemcpy(h_c, d_c, (sizeof(int)*m*k), cudaMemcpyDeviceToHost);
    //cudaThreadSynchronize();

    //开始stop2 Event
    CHECK(cudaEventRecord(stop2));
    //非阻塞模式
    //CHECK(cudaEventSynchronize(stop2));
    cudaEventQuery(stop2);

    //计算时间 stop-stop2
    float elapsed_time2;
    cudaEventElapsedTime(&elapsed_time2, stop, stop2);
    printf("stop-》stop2:Time = %g ms.\n", elapsed_time2);

    //销毁Event
    CHECK(cudaEventDestroy(start));
    CHECK(cudaEventDestroy(stop));
    CHECK(cudaEventDestroy(stop2));

    //CPU函数计算
    cpu_matrix_mult(h_a, h_b, h_cc, m, n, k);

    int ok = 1;
    for (int i = 0; i < m; ++i)
    {
        for (int j = 0; j < k; ++j)
        {
            if(fabs(h_cc[i*k + j] - h_c[i*k + j])>(1.0e-10))
            {
                
                ok = 0;
            }
        }
    }

    if(ok)
    {
        printf("Pass!!!\n");
    }
    else
    {
        printf("Error!!!\n");
    }

    // free memory
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
    cudaFreeHost(h_a);
    cudaFreeHost(h_b);
    cudaFreeHost(h_c);
    return 0;
}

编译时会有些警告,不用管它:

在Quardo P1000的GPU上执行:

在Jetson Nano B01上执行:

这里以矩阵乘为例,打印了调用矩阵乘核函数的时间,以及后面 cudaMemcpy的时间。

我们强行将

CHECK(cudaMemcpy(d_b, h_b, sizeof(int)*n*k, cudaMemcpyHostToDevice));

改为

CHECK(cudaMemcpy(d_b, h_b, sizeof(int)*n*k*2, cudaMemcpyHostToDevice));

故意让其出界。

再重新编译,运行,看看效果:

系统会告诉你 这行有错:

这样就可以跟踪出CUDA调用中的错误。

这里需要总结一下张小白在调试CHECK过程中发现的几个问题:

(1)如果没有 CHECK(cudaEventCreate()) 就直接调用 cudaEventRecord() 或者执行后面的Event函数,会导致打印不了信息。张小白当时对于stop2这个event就犯了这个错,导致 stop->stop2的时间怎么都打不出来。

(2)对于 cudaEventQuery() 是不能加 CHECK的,如果加了反而会报错:

在上面的环境中,如果您这样写:

CHECK(cudaEventQuery(stop2));

编译执行就会出现以下错误:

注:cudaEventQuery的cudaErrorNotReady代表了事件还没发生(还没有被记录),不代表错误。

使用Nano查看下性能:

!echo nano | sudo -S /usr/local/cuda/bin/nvprof ./matrix_mul

(二)Memory

这里讲的是CUDA存储单元,其实Day2也提到过:

今天再进一步说一下:

张小白整理了以下表格:(不见得全,还需要完善)

名称 位置 用途 使用方法 限制 备注
Register寄存器 GPU的SM上 存储局部变量 每个SM上有成千上万个

一个线程最大数量为256个
需要省着用
线程私有,最快
线程退出则失效
Shared memory GPU芯片上 实现Block内的线程通信,目前最快的多Thread沟通的地方 __shared__修饰符
需要__syncThreads()同步
分为32个banks
需要省着用,会影响活动warp数量
可被1个block所有thread访问,次快
高带宽,低延迟
Local memory 存放单线程的大型数组和变量(Register不够时用它) 没有特定的存储单元 线程私有,速度较慢,速度与Global memory接近
Constant memory
常量内存
驻留在device memory中 用于同一warp的所有thread同时访问同样的常量数据,比如光线追踪 __constant__修饰符

必须在host端使用 cudaMemcpyToSymbol初始化
没有特定的存储单元,但是有单独的缓存 只读,全局
Global memory 等同于GPU显存
驻留在device memory中
输入数据,写入结果 全局,速度较慢
Texture memory
纹理内存
用于加速局部性访问,比如热传导模型 只读,全局,速度次于Shared Memory(延迟比Shared Memory高,带宽比hared Memory小)
Host memory:
可分页内存
主机端内存 使用malloc访问使用free释放 不可以使用DMA访问 内存页可以置换到磁盘中
另一种Host memory:
又称:

Page-locked Memory,Zero-Copy Memory
主机端内存 使用cudaMallocHost访问
使用cudaFreeHost释放
属于另一种Global memory


(三)Shared Memory

这里主要介绍了如何使用Shared Memory优化CUDA应用

Shared Memory的特点是快的时候特别快,慢的时候特别慢。

什么时候快?

同一warp中所有线程访问不同的banks

或者 同一warp中所有线程读取同一地址(通过广播)

什么时候慢?

同一warp中多个线程访问同一个bank的不同地址(此时将产生 bank conflict

串行访问

请注意:bank conflict发生的原因就是 warp的分配和bank的分配重叠了:

如何避免bank conflict,简单的方法是Padding法(好像叫做补边):

通过增加一个空列,让bank强行错位,使得每段连续的数据被分配到不同的bank中。

具体做法很简单:

就是在设置Shared Memory的时候,不设置成 方阵BLOCK_SIZE X BLOCK_SIZE,而设置成 BLOCK_SIZE X (BLOCK_SIZE+1).

最后,我们可以使用Shared Memory优化mXn, nXk的矩阵乘 的代码,提高访存的效率。

具体方法如下:

申请两块 Shared Memory,都是BLOCK_SIZE X BLOCK_SIZE 大小。一个沿着矩阵mXn滑动,一个沿着矩阵 nXk滑动。将 子集的结果累加到 目的矩阵中:

具体的代码如下:

__global__ void gpu_matrix_mult_shared(int *d_a, int *d_b, int *d_result, int m, int n, int k) 
{
    __shared__ int tile_a[BLOCK_SIZE][BLOCK_SIZE];
    __shared__ int tile_b[BLOCK_SIZE][BLOCK_SIZE];

    int row = blockIdx.y * BLOCK_SIZE + threadIdx.y;
    int col = blockIdx.x * BLOCK_SIZE + threadIdx.x;
    int tmp = 0;
    int idx;

    for (int sub = 0; sub < gridDim.x; ++sub) 
    {
        idx = row * n + sub * BLOCK_SIZE + threadIdx.x;
        tile_a[threadIdx.y][threadIdx.x] = row<n && (sub * BLOCK_SIZE + threadIdx.x)<n? d_a[idx]:0;
        idx = (sub * BLOCK_SIZE + threadIdx.y) * n + col;
        tile_b[threadIdx.y][threadIdx.x] = col<n && (sub * BLOCK_SIZE + threadIdx.y)<n? d_b[idx]:0;
        
        __syncthreads();
        for (int k = 0; k < BLOCK_SIZE; ++k) 
        {
            tmp += tile_a[threadIdx.y][k] * tile_b[k][threadIdx.x];
        }
        __syncthreads();
    }
    if(row < n && col < n)
    {
        d_result[row * n + col] = tmp;
    }
}


并将前面 代码中调用矩阵乘的地方:

gpu_matrix_mult<<<dimGrid, dimBlock>>>(d_a, d_b, d_c, m, n, k);   

改为

gpu_matrix_mult_shared<<<dimGrid, dimBlock>>>(d_a, d_b, d_c, m, n, k);  

其余不变。

开始编译,在Jetson Nano B01上执行:

比较前面的矩阵乘代码,start-》stop:Time = 2.25109 ms 时间略有下降。

张小白于是修改blocksize,将其分别改为 16,8,4,再进行统计汇总:

矩阵MXN(m) 矩阵NXK(n) 矩阵NXK(k) blocksize stop-start(ms)
100 100 100 32 1.83286
100 100 100 16 1.27365
100 100 100 8 1.23292
100 100 100 4 3.52865
100 100 100 6(补测) 2.1999
100 100 100 12(补测) 1.34755

从上面的结果来看,blocksize为8,16,32时好像差异不大,但是blocksize为4的时候速度降得比较厉害。从100为4的倍数来看。貌似是这个时候wrap和bank重叠了。

那我们使用Padding大法看看:

将tile_a和tile_b的方阵改为补边的不方阵:

好像效果也不是很好。

注:在blocksize为4时,其实并没有发生bank conflict!而只是因为4X4,只有16个线程,而一个warp需要32个线程,所以相当于计算时,有一半算力被浪费掉了,进而速度慢了一倍。欢老师建议,至少应该NXN>32比较好。

于是张小白将blocksize设成6,又试了一下,结果插入了上述表格。当然,速度还是略有下降(下面也是一样)。我个人猜测,如果是六六三十六,其实32个线程一个warp,反而需要2个warp才能完成工作,所以速度还是不行。张小白猜想应该把blocksize的平方设成32的倍数是最合适的。比如八八六十四。。。12X12=32X4.5,好像也不大合适。。但是可能会因为使用较多而速度略有提高(事实证明好像也是如此)


张小白担心是矩阵太小的缘故,将 矩阵从100改为1000试试。

但是发现一旦改为1000后,CPU计算可能算不过来了:

只好将CPU那部分代码和后面比较的代码屏蔽掉。

再重新统计:

矩阵MXN(m) 矩阵NXK(n) 矩阵NXK(k) blocksize stop-start(ms)
1000 1000 1000 32 265.106
1000 1000 1000 16 228.09
1000 1000 1000 8 202.382
1000 1000 1000 4 518.315
1000 1000 1000 6(补测) 386.171
1000 1000 1000 12(补测) 246.29

张小白用Padding法试了一下:

好像也没有得到提速的效果:(反而更慢了)

注:同理,其实并没有发生什么bank conflict,都是张小白心里在YY。。

(未完待续)

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。