• CUDA教程一、认识cuda


    什么是cuda

    统一计算设备架构(Compute Unified Device Architecture, CUDA),是由NVIDIA推出的通用并行计算架构。解决的是用更加廉价的设备资源,实现更高效的并行计算。

    点击下面链接就可以下载cuda。我个人使用的是10.2版,截止到目前官方已经发布了11.0版。

    有人就问了,std::thread它不香吗,为什么要用cuda?

    别忘了,cuda是英伟达(NVIDIA)推出的——这个英伟达是何方神圣?没听说过英伟达,也应该听说过GPU了吧。没错,提出GPU概念的,正是英伟达。和中央处理器(Central Processing Unit, CPU)相对,图形处理器(Graphics Processing Unit, GPU)是显卡的核心芯片。而cuda正是暴露了英伟达开发的GPU的编程接口。

    几乎所有的编程语言,不使用特定框架,都只能实现CPU编程——std::thread也是将线程开在CPU中。而不同于每一位程序员都接触过的CPU编程,GPU编程可以使用更多的流处理器、更多的线程数。

    举个例子,打开我们的任务管理器,我们可以查看自己电脑CPU的线程数:

     而GPU的核数和线程数更是丰富,只要合理使用,几乎没有用得完的情况:

    如何合理使用,才最高效、利用率最高呢?

    从矩阵相乘说起

    举一个简单的例子,计算两个矩阵AB相乘的结果:

     我们知道:

    如果使用单线程编程,我们可以这么写:

     1 template <typename T>
     2 void MatrixProduct(matrix<T>& C /* Out */, matrix<T> const& A /* In */, matrix<T> const& B /* In */) {
     3     assert(A.Width() == B.Height());
     4     C.Set_size(A.Height(), B.Width());
     5     for(size_t i = 0; i < A.Height(); ++i) {
     6         for(size_t j = 0; j < B.Width(); ++j) {
     7             T ans = 0;
     8             for(size_t k = 0; k < A.Width(); ++k) {
     9                 ans += A[i][k] * B[k][j];
    10             }
    11             C[i][j] = ans;
    12         }
    13     }
    14 }

    这是毋庸置疑的,相信即使是编程初学者也可以写出。当然它在局部性上有待优化,不过优化空间非常非常小。

    如果是多线程编程呢?

    由于多个线程同时写入同一个变量是很危险的,但同时读取同一个变量不会出现问题,所以如果不想引入非常耗时的互斥锁,我们就必须根据可写入变量的数量来划分线程数——比如上式两个矩阵的乘积是[公式]的方阵,因此我们可以分成四个线程来计算,每个线程只计算结果矩阵的一个元素。代码如下:

     1 template <typename T>
     2 void thrd_MatrixProduct(matrix<T>& C /* Out */, matrix<T> const& A /* In */, matrix<T> const& B /* In */,
     3             size_t row, size_t col) {
     4 //每一个线程因row和col的不同而被区分
     5     T ans = 0;
     6     for(size_t k = 0; k < A.Width(); ++k) {
     7         ans += A[row][k] * B[k][col];
     8     }
     9     C[row][col] = ans;
    10 }
    11 
    12 template <typename T>
    13 void MatrixProduct(threadPool& tp, matrix<T>& C /* Out */, matrix<T> const& A /* In */, matrix<T> const& B /* In */) {
    14     assert(A.Width() == B.Height());
    15     C.Set_size(A.Height(), B.Width());
    16 //将每个计算单元的任务放进线程池中
    17     for(size_t i = 0; i < A.Height(); ++i) {
    18         for(size_t j = 0; j < B.Width(); ++j) {
    19             tp.addTask(thrd_MatrixProduct, C, A, B, i, j);
    20         }
    21     }
    22 //线程池启动并阻塞等待
    23     tp.start();
    24     tp.join();
    25 }

    要注意的是,threadPool和matrix类都不是STL,需要自行实现。其中threadPool可以用STL封装的线程std::thread或者C语言的pthread库来实现。std::thread类的声明中delete了拷贝构造函数和赋值运算符重载,这一点请在实现阶段注意。

    事实上,这个操作在数据量较小的时候看不出优势;而数据量过大时,线程调度又是操作系统的难题。我们需要可以容纳更多线程的廉价计算资源。

    cuda正是给显卡计算这一廉价而高效的并行计算方式提供了接口,同时也不需要线程池的维护。比如上述问题,用cuda实现,或许过程有点复杂,但核心还是相当容易的:

     1 template <typename T>
     2 __global__ void kern_MatrixProduct(T* C_ptr, const T* A_ptr, const T* B_ptr,
     3                    size_t A_Height, size_t A_Width, size_t B_Width) {
     4 //获取当前线程调用者的Id
     5     size_t Idx = blockIdx.x * blockDim.x + threadIdx.x;
     6     if(Idx >= A_Height * B_Width) return;
     7     
     8 //规定计算第i行j列的线程Id为(i * width + j)
     9     size_t row = Idx / B_Width;
    10     size_t col = Idx % B_Width;
    11 //核心计算
    12     T ans = 0;
    13     for(size_t k = 0; k < A_Width; ++k) {
    14         ans += A_ptr[row * A_Width + k] * B_ptr[k * B_Width + col];
    15     }
    16     C_ptr[row * B_Width + col] = ans;
    17 }
    18 
    19 template <typename T>
    20 void MatrixProduct(matrix<T>& C /* Out */, matrix<T> const& A /* In */, matrix<T> const& B /* In */) {
    21     assert(A.Width() == B.Height());
    22     C.Set_size(A.Height(), B.Width());
    23     
    24     T *A_ptr, *B_ptr, *C_ptr;
    25 //在显卡中申请内存(显存)
    26     assert(cudaSuccess == cudaMalloc(&A_ptr, sizeof(T) * A.Height() * A.Width()));
    27     assert(cudaSuccess == cudaMalloc(&B_ptr, sizeof(T) * B.Height() * B.Width()));
    28     assert(cudaSuccess == cudaMalloc(&C_ptr, sizeof(T) * A.Height() * B.Width()));
    29 //将数据从内存复制到显存
    30     assert(cudaSuccess == cudaMemcpy(A_ptr, A.pos_ptr(), sizeof(T) * A.Height() * A.Width(), cudaMemcpyHostToDevice));
    31     assert(cudaSuccess == cudaMemcpy(B_ptr, B.pos_ptr(), sizeof(T) * B.Height() * B.Width(), cudaMemcpyHostToDevice));
    32 //调用核函数
    33     size_t thread_count = 1024;
    34     size_t block_count = (A.Height() * B.Width() - 1) / thread_count + 1;
    35     kern_MatrixProduct<<<block_count, thread_count>>> (C_ptr, A_ptr, B_ptr, A.Height(), A.Width(), B.Width());
    36     cudaDeviceSynchronize();
    37     assert(cudaSuccess == cudaGetLastError());
    38 //将数据从显存复制到内存
    39     assert(cudaSuccess == cudaMemcpy(C.pos_ptr(), C_ptr, sizeof(T) * C.Height() * C.Width(), cudaMemcpyDeviceToHost));
    40 //释放临时内存
    41     assert(cudaSuccess == cudaFree(A_ptr));
    42     assert(cudaSuccess == cudaFree(B_ptr));
    43     assert(cudaSuccess == cudaFree(C_ptr));
    44 }

    当然,如果你在实现matrix的时候就把其元素数组申请在显存中,这个代码就会变得更加容易:

     1 template <typename T>
     2 __global__ void kern_MatrixProduct(T* C_ptr, const T* A_ptr, const T* B_ptr,
     3                    size_t A_Height, size_t A_Width, size_t B_Width) {
     4 //获取当前线程调用者的Id
     5     size_t Idx = blockIdx.x * blockDim.x + threadIdx.x;
     6     if(Idx >= A_Height * B_Width) return;
     7     
     8 //规定计算第i行j列的线程Id为(i * width + j)
     9     size_t row = Idx / B_Width;
    10     size_t col = Idx % B_Width;
    11 //核心计算
    12     T ans = 0;
    13     for(size_t k = 0; k < A_Width; ++k) {
    14         ans += A_ptr[row * A_Width + k] * B_ptr[k * B_Width + col];
    15     }
    16     C_ptr[row * B_Width + col] = ans;
    17 }
    18 
    19 template <typename T>
    20 void MatrixProduct(matrix<T>& C /* Out */, matrix<T> const& A /* In */, matrix<T> const& B /* In */) {
    21     assert(A.Width() == B.Height());
    22     C.Set_size(A.Height(), B.Width());
    23 //调用核函数
    24     size_t thread_count = 1024;
    25     size_t block_count = (A.Height() * B.Width() - 1) / thread_count + 1;
    26     kern_MatrixProduct<<<block_count, thread_count>>> (C.pos_ptr(), A.pos_ptr(), B.pos_ptr(), A.Height(), A.Width(), B.Width());
    27     cudaDeviceSynchronize();
    28     assert(cudaSuccess == cudaGetLastError());
    29 }

    最后一种写法效率是最高的——第一种没有使用并行,第二种的并行优势不明显(而且申请线程太多,消耗大量时间),第三种I/O的耗时过大。

    Windows10系统下的实验表明,当矩阵长宽小于10时,1、2、4三种写法的函数都能在0.01秒内完成,第三种也能在0.02秒内计算完毕。而当矩阵长宽为512时,第一种在1.1s左右计算完毕,第二种10s内计算不出结果,第三种则在1.3s左右结束,而第四种只有0.03s。

    如果说即使如此也看不出1、3、4三种方式的孰优孰劣的话,试想一下,如果是若干个矩阵连乘,效果会是如何?结果不言而喻:第三种最慢,因为频繁调用I/O;第一种次之,因为没有并行;第四种的优势将会极其明显,因为I/O少、并行效率高。

    很多人好奇,具体是如何并行的呢?上文代码中的三层尖括号(<<< >>>)是什么?莫急,让我一一解释。

    首先我们来了解显卡的结构。

    显卡的简化结构

    显卡内部,有三级结构:网格(grid)、块(block)、线程(thread)。

    每个显卡只有很少的网格,一个核函数目前只能运行在一个网格中,而一个网格里有多个块,每个块包含了若干线程。具体包含了多少,可以使用上文图中的deviceQuery来查询,通常是256、1024这样的值。

     同样是这个矩阵相乘的例子:

     上文的核函数在执行时,选取了一个网格中的一个块进行并行计算:

     执行核函数时GPU一个执行块的简化示意图,大多数线程都被直接return掉

    关于显卡的介绍到此为止,太过深入的知识我将不作介绍。

    如何将计算部署在这些块和线程之上呢?这就是核函数(kernel function)及其运行时参数(runtime parameter)的工作。

    核函数及其运行时参数

    相信大家也猜到了,核函数就是上文代码里有__global__修饰的函数,其运行时参数就是放在三重尖括号<<< >>>之中的值。

    每次调用核函数,都要指定其运行时参数,以便在显卡中弹性地部署计算。而其函数参数和我们学过的C/C++写法是一样的,不过要写在运行时参数的后面。

    运行时参数有两种写法:

    整数的写法:第一个参数是部署的核数[公式],第二个参数是每个核分配的线程数[公式],即在调用时:

    kernelFunc<<<b, t>>> (arg1, arg2);

      注意不能超过每个核的最大线程数量,否则cudaGetLastError会返回一个

      cudaErrorInvalidConfiguration错误。运行时通过blockIdx.x * blockDim.x + threadIdx.x来获取线程ID,其区间范围为。如果的值大于你的预期,请在不必要的线程中及时return,以免出现数组越界、访问无效内存等情况。有人可能会问,我挨个指定每个核开辟多少线程不行吗?不行。核函数的线程,往往都是要稍微开多一些,然后return掉不必要的。英伟达就是这么霸道,我不要你觉得,我要我觉得。

      三维向量的写法:第一个参数指定了网格中的维度,即如何分配块;第二个参数指定了每个块的维度,即如何分配线程;第三个参数可选,指定了核函数运行时每个block除了使用共享显存外,还能再动态申请多少空间,默认是0,单位是字节——这和malloc函数参数的意义一样;第四个参数也是可选的,指定了该核函数运行在哪一个流中,默认是0——这个流方便主显异步并行,但相对麻烦,暂时不作介绍。

      其中的类型是dim3,即三维无符号整数向量:x坐标表示该网格中沿x坐标的核数,即每行的核数;y坐标表示该网格中沿y坐标的核数,即每列的核数;z坐标只能为1,因为网格中每一个核都是平面摆放的。因此核函数部署的核数就是

      而的类型也是dim3:x坐标表示一个核中沿x坐标的线程数,即每行的线程数;y坐标表示一个核中沿y坐标的线程数,即每列的线程数;z坐标表示一个核中沿z坐标的线程数,即每个核的线程有多少个水平面。因此核函数在每个核开辟的线程数就是线程ID的值的取值范围则在区间中。

      而这些值都是有最大值的,同样也可以通过deviceQuery查询,我的显卡中五个参数的最大值分别为65536、65536、1024、1024、64。

      而核函数运行在显卡中,这就限制了它访问内存中的变量——但我们可以将内存中的变量传递到显存中。下一篇文章将会介绍主存与显存的交互。

  • 相关阅读:
    【计网实验6】静态路由实验
    【计网】第六章
    【操统5】第六章/第七章
    【计网 6】链路层
    【Java学习1】
    【机器学习1】
    【计网实验】packet
    【计网】第五章网络层:控制平面
    python中math模块常用的方法整理
    使用python如何进行截小图
  • 原文地址:https://www.cnblogs.com/ybqjymy/p/15883344.html
Copyright © 2020-2023  润新知