理解GPU(CPU VS GPU)
可以看一下youtube视频(https://www.youtube.com/watch?v=lGefnd7Fmmo), https://www.youtube.com/watch?v=YEWhbGLNgG0
cpu(延迟机) gpu(吞吐机 throughput)

CPU, 通过个人电脑是8c 16g,生产上有大规格的如64c128g等,gpu则是加大了Core(上千,A100 6912个core),远远大于cpu机器.从图上可以看,GPU 的 Core 数量要远远多于 CPU,意味着GPU能有更大的并行度,但是有得必有失,可以看到 GPU 的 Cache 和 Control 要远远少于 CPU,这使得 GPU 的单 Core 的自由度要远远低于 CPU,会受到诸多限制,而这个限制最终会由程序承担。这些限制也使得 GPU 编程与 CPU 多线程编程有着根本区别,在上右图中看出,每一行有多个 Core,却只有一个 Control,这代表着多个 Core 同一时刻只能执行同样的指令,这种模式也称为 SIMT (Single Instruction Multiple threads).
因此从 GPU 的架构出发,我们会发现,因为 Cache 和 Control 的缺失,只有 计算密集 与 数据并行的程序适合使用 GPU。
计算密集:数值计算 的比例要远大于 内存操作,因此内存访问的延时可以被计算掩盖,从而对 Cache 的需求相对 CPU 没那么大(CPU花费了大量的精力优化了内存延迟的问题)。

数据并行:大任务可以拆解为执行相同指令的小任务,因此对复杂流程控制的需求较低。
所以gpu比较讨厌if else等控制流程,也不适合字符串、udf等一些复杂的任务.
而深度学习恰好满足以上两点,即使存在比深度学习计算量更低且表达能力更强的模型,但如果不满足以上两点,都势必打不过 GPU 加持下的深度学习。
CPU 与 GPU交互
CPU主要从主存(Main Memory)中读写数据,并通过总线(PCIe Bus)与GPU交互。

GPU核心在做计算时,只能直接从显存中读写数据,程序员需要在代码中指明哪些数据需要从内存和显存之间相互拷贝。这些数据传输都是在总线上,因此总线的传输速度和带宽成了部分计算任务的瓶颈。也因为这个瓶颈,很多计算任务并不适合放在GPU上,比如推荐系统虽然也在使用深度学习,但因为输入是大规模稀疏特征,GPU加速获得的收益小于数据互相拷贝的时间损失。当前最新的总线技术是NVLink,IBM的Power CPU和英伟达的高端显卡可以通过NVLink直接通信。同时,单台机器上的多张英伟达显卡也可以使用NVLink相互通信,适合多GPU卡并行计算的场景。所以针对多GPU卡训练要使用NVLINK通信而不是PCIE通信才能更好发挥多GPU的优势,不然很可能数据传输的耗时就已经超过了计算带来的优势

从代码执行角度来看:

gpu工作原理
gtx980, 原理类似

可以看到图中有4个memory controller,4个gpc,16个SMM
GPU 通过 PCIE Host Interface 读取 CPU 指令,GigaThread Engine 将特定的数据从 Host Memory 中拷贝到内部的显存中。随后 GigaThread Engine 创建并分发多个 Thread Blocks 到多个 SM(Streaming Multiprocesso)
上。多个 SM 彼此独立,并独立调度各自的多个 Thread Wraps 到 SM 内的 CUDA Cores 和其他执行单元上执行。
上面这句话有几个概念解释一下:
(1)SM(Streaming Multiprocesso): 对应于上图中的 SM 硬件实体,内部有很多的 CUDA Cores;

SM 内有 32 个 CUDA Cores,每个 CUDA Core 含有一个 Integer arithmetic logic unit (ALU)和一个 Floating point unit(FPU). 并且提供了对于单精度和双精度浮点数的 FMA 指令。
SM 内还有 16 个 LD/ST 单元,也就是 Load/Store 单元,支持 16 个线程一起从 Cache/DRAM 存取数据。
4 个 SFU,是指 Special Function Unit,用于计算 sin/cos 这类特殊指令。

每个SMM,有4个block, 每个block 有32个cores( 基本执行单元),共128个cores/SMM

16个SMM,总共就是2048个cores ,A100就到6912个cuda cores了

(2)Thread Block: 一个 Thread Block 包含多个线程(比如几百个),多个 Blocks 之间的执行完全独立,硬件可以任意调度多个 Block 间的执行顺序,而 Block 内部的多个线程执行规则由程序员决定,程同时程序员可以决定一共有多少个 Blocks;

(3)Thread Warp: 32 线程为一个 Thread Warp,Warp 的调度有特殊规则
Warp 调度单元

CPU编程写的程序一般是单线程串行执行的。在 SIMD(单指令多数据)中,一条指令同时适用于许多数据元素。而Nvidia GPU 采用SIMT(单指令多线程)模式进行并行计算。
我们首先需要写一个kernel函数,最后会创建出成千上万的线程,每个线程独立执行相同的kernel指令,但是处理不同的数据:


每个线程执行相同的指令,但是所有的线程是按照block和grid两个层次进行管理的:
add<<<grid_size, block_size>>> (a, b, c);
每个线程块block包含几十数百个线程(一般应是32整数倍),而线程块内部的线程又是以32个线程组成为一个warp进行执行的。同时一个warp内部的32个线程是比较严格同步执行的(每个线程同一个时刻执行相同的指令)。最后多个block组成了一整个grid
1 AI编译器
场景: 推理场景,训练场景

啥是AI编译器:
参考一篇综述论文:The Deep Learning Compiler: A Comprehensive Survey

这个图有密集,从图里面可以看见几个核心重点:
IR 中间表达
编译器主要分为前后端,分别针对于硬件无关和硬件相关的处理。每一个部分都有自己的IR
(Intermediate Representation,中间表达),每个部分也会对进行优化:
• High-level IR:用于表示计算图,其出现主要是为了解决传统编译器中难以表达深度学习模型中
的复杂运算这一问题,为了实现更高效的优化所以新设计了一套IR。
• Low-level IR:能够在更细粒度的层面上表示模型,从而能够针对于硬件进行优化,文中将其分
为了三类。
Frontend 前端优化(构图+优化)
构造计算图后,前端将应用图级优化。因为图提供了计算全局概述,所以更容易在图级发现和执
行许多优化。前端优化与硬件无关,这意味着可以将计算图优化应用于各种后端目标。前端优化
分为三类:
节点级优化,如Zero-dim-tensor elimination、Nop Elimination
块级优化,如代数简化、常量折叠、算子融合
数据流级优化,如Common sub-expression elimination、DCE
Backend 后端优化
特定硬件的优化
• 目标针对特定硬件体系结构获取高性能代码。1)低级IR转换为LLVM IR,利用LLVM基础结构生
成优化的CPU/GPU代码。2)使用领域知识定制优化,这可以更有效地利用目标硬件。
自动调整
• 由于在特定硬件优化中用于参数调整的搜索空间巨大,因此有必要利用自动调整来确定最佳参
数设置。1)Halide/TVM允许调度和计算表达分开,使用自动调节来得出较佳配置。2)应用多
面体模型Polyhedral model 进行参数调整。
优化内核库
• 厂商特定优化内核库,广泛用于各种硬件上的加速DL训练和推理。特定优化原语可以满足计算
要求时,使用优化的内核库可显著提高性能,否则可能会受到进一步优化的约束。

从下往上分析,最低就是硬件如X86CPU,GPU等,在此之上为kernel层,可以做Kenel优化,往往一些厂商比如英伟达提供了cuDNN的gpu库等,之后就是GraphLevel,我们可以做图层编译优化如常量折叠、算子融合、公共子表达式消冗xxx等,最上面就是框架层比如tf, torch等等。
上面就是对于AI编译的基本理解.
AI编译器与传统编译器的区别是啥
首先他们具有相似性:
目标相同:通过自动化方式进行程序优化和代码生成,从而降低对不同硬件的手工优化;
优化方式类似:在编译优化层通过统一IR 执行不同的Pass进行优化,从而提高执行性能;
软件结构栈类似:分成前端、优化、后端三段式,IR 解耦前端和后端使得模块化表示;
AI编译器依赖传统编译器:AI编译器对Graph IR 进行优化后,将优化后的IR 转换成传统编译
器IR,最后依赖传统编译器针进行机器码生成。
差别:
传统编译器:


传统编译器:
- 输入是高级语言如java、python、c++,输出是低级语言机器码
- 优先降级编程难度,然后是优化性能
AI编译器:
- 输入是计算图/算子,输出是低级语言机器码
- 主要是优化性能(支撑AI高效计算、大模型计算),再然后是优化易用性
一张图表达差异点点:

举例:
IR 差异:AI 编译器的IR 与传统编译器的IR所抽象出来的概念和意义并不相同。
◦ AI编译器一般会有high-level IR,用来抽象描述深度学习模型中的运算,如:Convolution、Matmul 等,
甚至部分会有Transformer 带有图的结构。
◦ 传统编译器相对而言low-level IR,用于描述基本指令运算,如load、store 等。有了high-level IR,AI编
译器在描述深度学习模型类DSL 更加方便。
优化策略:AI 编译器面向AI领域,优化时引入更多领域特定知识,从而进行更high-level,更加
aggressive 优化手段。如:
◦ AI编译器在high-level IR 执行算子融合等优化手段,缺点是可能会导致调试执行信息跟踪难,定位困难,可能会出现一种情况就是算法遇到了bug,但是发现报错看不懂或者和自己所想的bug不同;
◦ AI编译器可以降低计算精度(可以想想当前的大模型,2b模型、6b模型、13b模型、16b模型xxx),比如int8、fp16、bf16等,因为深度学习对计算精度不那么敏感。但传统编译器一般不执行改变变量类型和精度等优化。
AI 编译器发展的3个阶段:

阶段1:
TensorFlow 早期版本,基于神经网络的编程模型,主要进行了graph 图和ops 算子两层抽象。
图层:通过声明式的编程方式,以静态图方式执行,执行前进行硬件无关和硬件相关的编译优化。硬件
无关的优化,如表达式化简、常量折叠、自动微分等;硬件相关的优化包括算子融合、内存分配等。
算子层:通常采用手写kernel 的方式,如在NVIDIA GPU 上基于CUDA kernel 实现大量的.cu 算子或
者依赖于CuDNN 算子优化库。

前端python code => Graph IR( 优化) => OP Kernel 算子 => AI芯片
这个阶段你会发现一些问题,当你使用tf1.0版本的时候,你会发现你需要学习tf的python前端API,和普通的python代码不太一样,本质是早期tf1.0为了解决AI问题自己定制了一套框架,它存在的问题:
表达上:
◦ 静态图的表达式非Python 原生,开发者主要通过框架提供Python API 显示构图,易用性上不好;
性能上:
◦ DSA 专用加速芯片出现加剧了性能上的挑战; 本质来说就是该框架可能在某些AI芯片上性能极强,但在某些AI芯片就不好使了。
◦ 算子层提供的算子粒度和边界提前确定后,无法充分发挥硬件的性能;
◦ 硬件厂商的提供的算子优化库也未必最优,所以可以看到现在大量的AI编译器在底层又大量撸了一大把的底层算法库
1)模型和shape 确定情况下,可能还有更优算子实现;
2)在SIMT 和SIMD 架构中,Scheduling、Tilling 都有有很大的空间。
阶段2

表达分离:计算图层和算子层仍然分开,算法工程师主要已关注图层的表达,算子表达和实现主
要是框架开发者和芯片厂商提供。
平衡效率和性能:算子实现上在Schedule、Tiling、Codegen 上缺乏自动化手段,门槛高,开发
者既要了解算子计算逻辑,又要熟悉硬件体系架构。 搞编译器框架你需要了解算法,同时你还需要了解硬件体系才能更好充分利用计算性能
阶段3 将来的AI编译器
图算统一表达,实现融合优化
算子实现上自动Schedule、Tiling、Codegen,降低开发门槛
更泛化优化能力,实现动静统一、动态Shape、稀疏性、高阶微分、自动并行等
包括编译器、运行时,异构计算、边缘到数据中心都模块化表示和组合,并专注于可用性

总结:
一个AI计算,首先要有输入—计算图,然后要进行前端优化,再然后进行后端优化。那么围绕这些点一点点展开。
2 AI框架-计算图
三大块内容:
为什么要用AI计算图
怎么用计算图表达自动微分
计算图的优化以及计算图的执行调度优化
为什么要有AI计算图:




计算图的作用:
从我的理解来看:计算图很明显是承上启下。向下硬件执行计算统一表示,向上承接AI相关程序表示
计算图的基本组成(张量Tensor Dataset/ OP/DAG)
(1) Tensor
基本数据结构:Tensor 张量
高维数组,对标量,向量,矩阵的推广
žTensor形状(Shape): [3, 2,5]
ž元素基本数据类型(DataType):int, float, string, etc.


优点:
后端自动推断并完成元素逻辑存储空间向物理存储空间的映射
基本运算类型作为整体数据进行批量操作,适合单指令多数据(SIMD)并行加速
通俗来说由两部分组成,张量(有的人喊它叫张量,有的人喊他叫Dataset 也就是分布式数据集),其次就是算子OP(除了大家理解的加减乘除是算子之外,梯度计算等也是算子)。
张量我的理解是类型一致加确定,并且可以打破分布式常见的行处理,变成列处理,块处理们可以进行更多的优化。
(2)OP
基本运算单元:Operator 算子
由最基本的代数算子组成
根据深度学习结构组成复杂算子
N个输入Tensor,M个输出Tensor

(3)DAG
DAG**** 表示计算逻辑和状态
节点代表 Operator
边代表 Tensor
特殊的操作
如:For/While 等构建控制流
特殊的边
如:控制边表示节点间依赖

怎么用计算图做自动微分
学习之前先看看深度学习训练的流程:


通过正向计算,以及损失函数计算进行反向计算,不断地重复寻找到局部最优解。
回到求导本身:


上面的代码就是构造了正向的计算模型(算子+forward前向计算)。 后向计算怎么算?先看看怎么进行求导,有如下两种方法:
(1) 符号微分,精度高,但复杂

(2) 数值微分

一些研究学者就开始搞出来了一个自动微分











图优化和图执行调度



计算图的优化主要通过编译来做的. 原始的计算图通过常量折叠、常量传播、算子融合、表达式简化、xxx方式完成优化,进行减zhi生成新的计算图,简单描述如下:
常量折叠:比如1+1 = 2, 这种情况没必要在运行时去算,而是在编译时期就把他算出来。
表达式替换比如x * x * x可能换成x^3之类的
公共表达式消除就是消除冗余表达式计算.



多设备:


从最想要的结果来看肯定希望第三种。
给个案例:

代价模型:本质是根据数据的规模、算子不同,会出现类似组合问题,如何达到最优组合,本身就是一个复杂的问题.

控制流
整个AI编译器除了算子(梯度微分、OP)外还需有一个事情,就是解决if, for, while之类的控制流程. 这往往还牵扯到动态图、静态图、动静统一相关的话题.

如上面代码所示:可以看到有一些if else的控制流。
现在的AI框架比较多,主流会使用3种

第一种的代表就是tf
第二种代表是torch
第三种代表是shengsi、 后来,tf torch也开始引入
1 tf的控制流(依赖原语)


if, for 转换成控制流原语 
优缺点:
优点:编译期直到全计算,可以更好地优化,提高效率

2 torch的控制流(用前端宿主语言的控制流)
模型即代码
复用前端的控制流,比如就是用python的控制流
神经网络的理解就是你的代码,没有多大的GAP,不用刻意学习其他的东西


控制流直接用poython的if else while for,好写,你想的啥样写的就是啥样
python很灵活,容易写不好
语言边界:cpu于gpu的通信,比如 s = x+y 在cpu, s = torch.square(y)在gpu,边界跳转

控制流python代码在前端,数据流代码在后端(c++)跨语言交换
3 源码解析,展开计算图(夹在中间)
有一定程度的自由使用
for 两次,就变成顺序的计算图
if else 两个子图,动态选择子图

AI 动静统一
动态图转静态图

trace
执行代码来追踪解析成静态图: 感觉不是很好用

AST JIT(即时编译)
code ->语法解析 -> 内部语法树 -> GraphIR(优化) -> 机器码
内部语法树:internal ast
经过graph优化转化为机器code
放到运行态执行

未来方向


现在大部分数据处理还是在CPU上执行,然后再丢给我们的硬件,进行AI计算,数据怎么使用AI芯片和计算图融合呢,是一个比较有挑战性的问题

3 AI框架-前端优化
图优化: 通过图的等价变换化简计算图,从而降低计算复杂度或者内存开销
GraphIR作为承上启下的中间表示,可以允许任何等价图优化PASS去化简计算流图或提高执行效率

表达式化简:

公共子表达式消除

Operator Batch


算子融合

子图替换

4 AI框架-后端优化(内核优化)



















暂无评论内容