GPU CUDA 编程的基本原理是什么

描述

作者:Keepin

1、cuda是英伟达开发的一套应用软件接口(API)。其主要应用于英伟达GPU显卡的调用。

2、云计算可以简单的理解为是通过网络组合成的计算机集群,用于各种加速,其中以CPU为主,GPU为辅。所以CUDA可以成为云计算的一个支柱。

3、神经网络能加速的有很多,当然使用硬件加速是最可观的了,而目前除了专用的NPU(神经网络加速单元),就属于GPU对神经网络加速效果最好了:

3.1、Cuda的整套软件非常完善,所以早期很多厂家用cuda能快速搭建神经网络加速软件框架,所以cuda也被广泛应用于神经网络加速。(当然英伟达还专门开发了一套神经网络套件,叫做tensorrt,在神经网络推理部署的时候更为广泛应用)

3.2、从开发api来说,神经网络的加速不仅仅可以用cuda加速,还可以用其他API,例如GPU开发API很多,包括opencl,opengl,vulkan都可以在英伟达的GPU上加速神经网络,只不过在英伟达显卡上整体来说开发复杂,这些API优化效率不如cuda,毕竟cuda是英伟达自家开发的,自然适配地非常好

3.3、从硬件角度来说,AMD的显卡也可以用于加速神经网络,只不过软件生态做得不如英伟达,自然应用程度也不如英伟达。事实上,AMD也开发一套Miopen的神经网络加速套件,类似于tensorrt;还开发了一套rocm套件,类似于cuda,只不过市场占有率较低而已。

3.4、amd和nvidia都是以桌面端的GPU为主,在移动端,例如手机芯片,高通、苹果和arm的GPU也可以用于加速神经网络,主要使用opencl,vulkan,metal等开发API来实现。

cuda的官方文档:https://docs.nvidia.com/cuda/

学习路线

sazc

入门CUDA

说到入门,个人比较推荐《CUDA C编程权威指南》,虽然这本书年代比较久,原版书2014年出版的,使用的GPU最新也仅仅是2012年的CC3.X的Kepler,但这些基础知识仍然是掌握CUDA最新特性必不可少的,因此还是值得当作入门学习读物的。

进阶CUDA

CUDA C++ Programming Guide

要想Coding出高质量的Kernel,那么官方最新的《CUDA C++ Programming Guide》便是让我们可以全面了解到CUDA特性的最直接的办法,目前最新的版本是12.2。

注:此手册个人认为适合随时查阅,用到啥查啥,从头开始阅读学习,会耗费巨大的精力,效果甚微。

CUDA Samples

如果想利用CUDA特性,快速搭建“简单”的Kernel原型,那么官方的《CUDA Samples》便是再好不过的参考资料。

神经网络

注:笔者曾经简单过了一遍这个repo,后面有任务需要编写分类算法、理解CUDA特性、学习CUDA 扩展库等,均从中找到了合适的Demo,快速搭建原型。

CUDA: NEW FEATURES AND BEYOND

如果对每代CUDA最新特性感兴趣,那么每年一次或两次的官方GTC发布的《CUDA: NEW FEATURES AND BEYOND》便是可以从宏观层面快速了解到该特性的最好方法之一。

注:观看视频,下载PDF需要注册,比较老的版本 Google可以搜到下载链接。

CUDA Library

cuDNN、cuBlas、cuDLA...。

GPU架构

搞CUDA的要不理解一些GPU的特性,那么永远写不出优质的代码,学习GPU架构可以从白皮书、GTC、官方Blog以及论坛的暗示入手。

SASS

官方的Binary Utilities与论坛的暗示、开源的Assembler、泄漏的SASS规格书、工具分析。

性能分析工具

Nsight System、Nsight Compute、Nsight VS Code等。

专业CUDA

答主目前还没到这个阶段,暂且挖坑。

学习资料

最近有读者伙伴在问我关于GPU的资料,这里分享三本电子书。仅作临时学习使用,请购买正版书籍,支持原创作者。

公众号回复获取电子书:GPU

GPU CUDA 编程的基本原理是什么?

作者:董鑫

想学好 CUDA 编程, 第一步就是要理解 GPU 的硬件结构, 说到底, CUDA 的作用就是最大程度压榨出 NVIDIA GPU 的计算资源.

想要从零理解起来, 还有有些难度. 这里希望能够用最简单的方式把一些最基本的内容讲清楚. 所以, 本文以易懂性为主, 牺牲了一些完全准确性.

GPU 结构

神经网络

这是 GPU 的基本结构. CUDA 编程主打一个多线程 thread.

多个 thread 成为一个 thread block, 而 thread block 就是由这么一个 Streaming Multiprocessor (SM) 来运行的.

CUDA 编程中有一个 "同一个 block 内的 thread 共享一段内存". 这恰好对应上图下面的那段 "Shared Memory".

另外, 一个 SM 里面有多个 subcore (本图中有 4 个), 每个 subcore 有一个 32 thread 的 warp scheduler 和 dispatcher, 这个是跟 CUDA 编程中 warp 的概念有关的.

另外, 我们还要理解 GPU 的金字塔状的 Memory 结构.

神经网络

最快的是 register, 但是非常小.

其次是 l1 cache, 也就是前面提到的 shared memory.

Global memory 就是我们常说的 显存 (GPU memory). 其实是比较慢的.

Global memory 和 shared memory 之间是 L2 cache, L2 cache 比 global memory 快. 每次 shared memory 要到 global memory 找东西的时候, 会去看看 l2 cache 里面有没有, 有的话就不用去 global memory 了.

有的概率越大, 我们说 memory hit rate 越高, CUDA 编程的一个目的也是要尽可能提高 hit rate. 总的来说,

神经网络

我们就是希望能够尽可能多的利用比较快的 SRAM (shared memory). 但是因为 SRAM 比较小, 所以基本原则就是: 每次往 SRAM 移动数据的, 都可能多的用这个数据. 避免来来回回的移动数据.

其实, 这种 idea 直接促成了最近大火的 FlashAttention. FlashAttention 发现很多操作计算量不大, 但是 latency 很高, 那肯定是不符合上述的 "每次往 SRAM 移动数据的". 怎么解决呢?

Attention 基本上是由 matrix multiplication 和 softmax 构成的. 我们已经知道了 matrix multiplication 是可以分块做的, 所以就剩下 softmax 能不能分块做? softmax 其实也是可以很简单的被分块做的. 所以就有了 FlashAttention. 关于 GPU Architecture 更进一步细节推荐大家看这个:

https://www.youtube.com/watch?v=Na9_2G6niMw&t=1166s

一些跟 CUDA 有关的库

首先先理一下 NVIDIA 那么多库, 他们之间到底是什么关系?

这里以三个库为例 CuTLASS , CuBLAS, CuDNN .

从名字看, 他们都是用 CUDA 写出来的.总结起来, 他们之间的区别就是封装程度不同, 专注的领域不同.

CuTLASS 和 CuBLAS 都是比较专注于最基本的线性代数的运算的, 比如 矩阵乘法, 矩阵加法, 等等.

而 CuDNN 则是更进一步, 将关注点落在了 Deep Neural Network 上面了. 比如, DNN 中非常常见的 convolution 操作, 是通过 矩阵乘法 来实现的.

那么, CuDNN 做的工作更像是, 完成从 convolution 到 矩阵乘法 的转换, 转换之后, 就可以复用 CuTLASS 或者 CuBLAS 来做.

当前, DNN 里面不全都是矩阵乘法, 遇到一些特殊的算子, CuDNN 也会自己实现一些.而 CuTLASS 和 CuBLAS 之间的区别, 更像是”傻瓜程度” 不同.

CuBLAS 可以理解成 英伟达 给你找了一堆傻瓜相机, 每个相机都针对某个非常非常具体的场景优化到了极致, 你唯一要做的就是根据你的场景找到最适合相机.

因为每个相机已经被写死了, 你不知道英伟达到底做了什么黑魔法优化, 你也没办法去根据想法进一步修改.

虽然 CuBLAS 闭源, 不灵活, 但是只要场景找对了, 效果确实拔群.

而 CuTLASS 是一个模板库, 相当于有这么一些不同 系列 的相机, 有拍夜景, 拍近景的, 等等. 当然, 就夜景而言, 肯定还有各种不同场景, 对应不同配置可以尝试. 在 CuTLASS 你可以尝试不同的配置, 直到你满意为止.

矩阵乘法 (CuTLASS)

假设做 GEMM (general matrix multiplication), 我们以 CuTLASS 里面的实现来讲解

C = A * B

A 是 M x K

B 是 K x N

神经网络

这张图的信息量很大.

1. 第一部分:

是关于如何拆分一个大矩阵乘法到多个小矩阵乘法. 也就是说, 这段时间我们就 focus on 某个小矩阵的计算. 其他部分我们不管.

这样, 因为不同小矩阵之间是完全独立的, 就可以很容易实现并行.

具体来说 也是需要多次紫色和黄色部分的矩阵乘法. 紫色从左到右滑动, 黄色从上到下滑动. 他们的滑动的次数是完全一样.

他们每滑动一次, 就是一次矩阵乘法(及加法, 因为要把乘出来的结果加到之前的结果之上). 下图是滑动到某个位置的情况.

之所以绿色部分没有高亮, 是因为要把所有都滑动完, 才能得到最终绿色部分的结果.

神经网络

2. 第二部分:

神经网络

先忽略绿色部分.注意, 这紫色的一整行和黄色的一整列都是由一个 thread block 处理的.为什么? 因为他们都要在同一块 C tile 上面累加.

如果切换 thread block 的话, 就会比较麻烦.理论上来说, 紫色的一整行和黄色的一整列都要被 load 到 shared memory.

但是, 因为我们每次只会用到 紫色高亮部分 和 黄色高亮部分, 所以完全可以声明一块小内存, 每次只 load 一小块 (A tile and B tile), 用完之后就可以覆盖掉, 用来 load 下一个滑动位置.具体到某一次滑动,

把 紫色高亮部分 (A tile) 和 黄色高亮部分 (B tile) 从 global memory 移动到 shared memory (SMEM).接下来要做的就是计算 A tile * B tile, 然后把结果累加到 C tile 上面.好了, 现在我们明确两点:A tile 和 B tile 都在 shared memory 了.

也就是说, thread block 里面的每一个 thread 都可以看到 A tile 和 B tile.A tile 和 B tile 每次窗口滑动是需要重新 load 的,

但是 C tile 每次窗口滑动是需要在原来的基础上更新的.因为 C tile 一直需要更新, 而且这个更新是计算驱动的 (不是 load 驱动的),

所以我们最好把 C tile 放到 register (RF) 里面去. register 是一种比 shared memory 更快的内存, 而且 register 是跟 thread 相关的.

其实计算的时候, A tile 和 B tile 也必须从 shared memory 被 load 到 register. 只是 C tile 一直待在 register 里面等待被更新.

3. 第三部分:

刚刚讲了, 我们需要把 C tile held 在 register, 而 register 有是跟具体某个 thread 是关联的. 所以我们自然需要根据 thread 再来划分 C tile.

但是, CUDA 其实在 thread 和 thread block 还有一层叫做 warp. 我们可以暂时理解把 warp 一种计量单位: 1 warp = 32 thread.在上述例子中, 一个 thread block 被分成了八个 warp.每个 warp 承担了 C tile 中的 16*8 个数字.

假设每个数字是 32bit, 那么恰好是一个 register 的容量.那么, 也就是说, 我们需要 16*8 个 registers. 前面讲了一个 warp 是 32 thread. 这并不矛盾. 因为一个 thread 可以配有多个 registers.

但是, 某个 thread 是不定访问 其他 thread 的 register 的. CUDA 之所以这么限制, 主要原因还是 thread 之间做 sync 太复杂.这里, 要算一个 warp 对应的结果, 还是跟前面的讲的一样. 横向滑动紫色高亮块, 纵向滑动黄色高亮块,

每次滑动, 都是将当前高亮块从 shared memory load 到 register 中, 计算矩阵乘法 (虽然看着像向量, 但是不一定是向量), 然后累加到 C warp tile 上面. 正如前面所说, C warp tile 一直都是在 register 上面的.

4. 第四部分:

最终, 还是要落到单个 thread 上面.

神经网络

这里的 Warp tile 代表这一整个 tile 都是由一个 Warp 完成的.先看左上角灰色的部分, 后面的内容也是在这个灰色的部分.数一下, 恰好是 32 个格子.

而我们知道一个 Warp 是 32 个 thread. 也就是说, 一个 thread 处理一个灰色的格子.  这 32 个格子中, 有一个绿色的格子 (先不要看另外三个绿色的),

这个绿色的格子就是我们现在要讨论的 32 个 thread 中的某个 thread.实际上, 这个绿色的格子里面有 16 个数, 也就说是一个 44 的迷你矩阵.

也就说, 一个 thread 要处理这样一个 44 的矩阵. 那么, 既然想得到这样 44 的矩阵, 就需要读入 41 的紫色部分, 1*4 的黄色部分.

好, 现在我们可以跳出左上角灰色部分了. 我们发现这个 Warp tile 还有剩下三个部分, 但是在左上角灰色部分我们已经这个 Warp 中 32 个 thread 用过一遍了, 怎么办?

谁说 Warp 只能用一遍? 完全可以让这个 Warp keep alive, 再如法炮制地去分别按顺序处理 (所以这里其实在时间上不是并行的) 剩下的三个部分.这个时候, 我们再看一个thread的工作量.

首先, 一个 thread 要被用 4 次, 然后每次要处理 4x4 的迷你矩阵,

所以总的来看, 一个 thread 相当于处理了一个 8x8 的矩阵. 这就是上图右边想表达的意思.

审核编辑:黄飞

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分