mirror of
https://github.com/chenzomi12/aisystem.git
synced 2025-10-20 21:03:47 +08:00
Merge branch 'main' of https://github.com/Infrasys-AI/AISystem
This commit is contained in:
@ -18,7 +18,7 @@ AI 编译器整体架构图如图所示。在图中最上层,AI 框架前端
|
||||
|
||||
在如下图所示的 AI 编译器前端优化流程图中,AI 编译器将对输入的 GraphIR,依次执行包括但不限于常量折叠、常量传播、算子融合、表达式简化、表达式替换、公共子表达式消除等各种前端优化 Pass,各个 Pass 的执行结果仍然为 GraphIR 并将输入到下一个 Pass 中,直到前端优化结束并输出最终优化后的 GraphIR。
|
||||
|
||||

|
||||

|
||||
|
||||
## 本节视频
|
||||
|
||||
|
@ -137,7 +137,7 @@ class Network(nn.Cell):
|
||||
|
||||
1. PyTorch 的 TorchDynamo 具有良好的设计和较为完善的功能,它可以解决多少实际问题,能够解决 99% 的场景下的问题吗?
|
||||
|
||||
2. MindSpore 的优化主要针对静态图+AI 编译器的模式,对于动态图转静态图与 AI 编译器结合的模式,是否有更好的方案?
|
||||
2. MindSpore 的优化主要针对静态图 + AI 编译器的模式,对于动态图转静态图与 AI 编译器结合的模式,是否有更好的方案?
|
||||
|
||||
## 小结与思考
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
## 算子融合方式
|
||||
|
||||
在讨论算子融合之前,我们首先要知道什么是计算图,计算图是对算子执行过程形象的表示,假设 $C = \{ N, E, I, O\}$ 为一个计算的计算表达,那么可以有:
|
||||
在讨论算子融合之前,我们首先要知道什么是计算图。计算图是对算子执行过程形象的表示,假设 $C = \{ N, E, I, O\}$ 为一个计算的计算表达,那么可以有:
|
||||
|
||||
- 计算图表示为由一个节点 N(Node),边集(Edge),输入边(Input),输出边(Output)组成的四元组
|
||||
- 计算图是一个有向联通无环图,其中的节点也被称为算子(Operator)
|
||||
@ -168,46 +168,46 @@ $$
|
||||
- BN 计算:
|
||||
|
||||
$$
|
||||
y = \gamma\frac{{\left( {z - mean} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
y = \gamma\frac{{\left( {z - \operatorname{mean}} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
$$
|
||||
|
||||
- ReLU 计算:
|
||||
|
||||
$$
|
||||
y=max(0,y)
|
||||
y=\operatorname{max}(0,y)
|
||||
$$
|
||||
- 融合卷积、BN 与 ReLU 的运算:
|
||||
|
||||
将卷积计算公式带入到 BN 计算公式中,可得到下式:
|
||||
|
||||
$$
|
||||
y = \gamma\frac{{\left( {(w*x+b) - mean} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
y = \gamma\frac{{\left( {(w*x+b) - \operatorname{mean}} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
$$
|
||||
|
||||
展开后可得到:
|
||||
|
||||
$$
|
||||
y =\gamma\frac{w}{{\sqrt {\operatorname{var}}}}*x+\gamma\frac{{\left( {b - mean} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
y =\gamma\frac{w}{{\sqrt {\operatorname{var}}}}*x+\gamma\frac{{\left( {b - \operatorname{mean}} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
$$
|
||||
|
||||
也即将卷积与 BN 融合后的新权重 $w'$ 与 $b'$,可表示为如下所示:
|
||||
|
||||
$$
|
||||
\begin{gathered}
|
||||
w' = \gamma\frac{w}{{\sqrt {\operatorname{var} } }} \hfill \\
|
||||
b' = \gamma\frac{{\left( {b - mean} \right)}}{{\sqrt {\operatorname{var} } }} + \beta \end{gathered}
|
||||
w' = \gamma\frac{w}{{\sqrt {\operatorname{var} } }} \\
|
||||
b' = \gamma\frac{{\left( {b - \operatorname{mean}} \right)}}{{\sqrt {\operatorname{var} } }} + \beta
|
||||
\end{gathered}
|
||||
$$
|
||||
|
||||
最后,将卷积、BN 与 ReLU 融合,可得到如下表达式:
|
||||
|
||||
$$
|
||||
\hfill \\
|
||||
y=max(0,w'*x+b') \hfill \\
|
||||
y=\operatorname{max}(0,w'*x+b') \\
|
||||
$$
|
||||
|
||||
## TVM 融合规则与算法
|
||||
|
||||
[TVM](https://github.com/apache/tvm)是一个端到端的机器学习编译框架,它的目标是优化机器学习模型让其高效运行在不同的硬件平台上。它前端支持 TensorFlow、Pytorch、MXNet、ONNX 等几乎所有的主流框架。它支持多种后端(CUDA、ROCm、Vulkan、Metal、OpenCL、LLVM、C、WASM)及不同的设备平台(GPU、CPU、FPGA 及各种自定义 NPU)。
|
||||
[TVM](https://github.com/apache/tvm) 是一个端到端的机器学习编译框架,它的目标是优化机器学习模型让其高效运行在不同的硬件平台上。它前端支持 TensorFlow、Pytorch、MXNet、ONNX 等几乎所有的主流框架。它支持多种后端(CUDA、ROCm、Vulkan、Metal、OpenCL、LLVM、C、WASM)及不同的设备平台(GPU、CPU、FPGA 及各种自定义 NPU)。
|
||||
|
||||
TVM 主要用于推理场景。在架构上,主要包括 Relay 和 TIR 两层。其通过 Relay 导入推理模型,随后进行融合优化,最后通过 TIR 生成融合算子。TVM 整体的算子融合策略是基于支配树来实现的,下面将介绍支配树等相关概念。
|
||||
|
||||
@ -218,15 +218,15 @@ TVM 主要用于推理场景。在架构上,主要包括 Relay 和 TIR 两层
|
||||
- 支配树:各个点的支配点构成的树
|
||||
- 支配点:所有能够到达当前节点的路径的公共祖先点( Least Common Ancestors,LCA)
|
||||
|
||||
具体而言,对于一张有向图(可以有环)我们规定一个起点 $r$,从 $r$ 点到图上另一个点 $w$ 可能存在很多条路径(下面将 $r$ 到 $w$ 简写为 $r→w$)。如果对于 $r→w$ 的任意一条路径中都存在一个点 $p$,那么我们称点 $p$ 为 $w$ 的支配点(也可以称作是 $r→w$ 的必经点),注意 $r$ 点不讨论支配点。
|
||||
具体而言,对于一张有向图(可以有环)我们规定一个起点 $r$,从 $r$ 点到图上另一个点 $w$ 可能存在很多条路径(下面将 $r$ 到 $w$ 简写为 $r→w$)。如果对于 $r→w$ 的任意一条路径中都存在一个点 $p$,那么我们称点 $p$ 为 $w$ 的支配点(也可以称作是 $r→w$ 的必经点)。注意,$r$ 点不讨论支配点,也就是说我们不把 $r$ 点算作任何点的支配点,也不讨论 $r$ 点自身的支配点。
|
||||
|
||||
下面用 $idom[u]$ 表示离点 $u$ 最近的支配点。对于原图上除 $r$ 外每一个点 $u$,从 $idom[u]$ 向 $u$ 建一条边,最后我们可以得到一个以 $r$ 为根的树。这个树我们就叫它"支配树"。
|
||||
下面用 $\text{idom}[u]$ 表示离点 $u$ 最近的支配点。对于原图上除 $r$ 外每一个点 $u$,从 $\text{idom}[u]$ 向 $u$ 建一条边,最后我们可以得到一个以 $r$ 为根的树。这个树我们就叫它"支配树"。
|
||||
|
||||
如下图所示,到达 Node8 的路径有 Node3->4->7->8,Node3->5->7->8,Node3->6->7->8,因此 Node4,Node5,Node6,Node7 为 Node8 的支配点。
|
||||
如下图所示,到达 Node8 的路径有 Node3->4->7->8,Node3->5->7->8,Node3->6->7->8,因此在 $\text{Node3}→\text{Node8}$ 中,Node7 为 Node8 的支配点。
|
||||
|
||||

|
||||
<img src="images/03OPFusion11.png" alt="TVM 示意图" width="500">
|
||||
|
||||
TVM 的算子融合策略就是检查每个 Node 到其支配点的 Node 是否符合融合条件,如果符合就对其进行融合。如上图,检查 Node4->Node7->Node8 是否能融合,若可以融合,则用新的算子替代原来路径上的算子。因此支配树作用如下:
|
||||
TVM 的算子融合策略就是检查每个 Node 到其支配点的 Node 是否符合融合条件,如果符合就对其进行融合。如上图,检查 Node3->Node7->Node8 是否能融合,若可以融合,则用新的算子替代原来路径上的算子。因此支配树作用如下:
|
||||
|
||||
- 检查每个 Node 到其支配点的 Node 是否符合融合条件
|
||||
- 融合的基本规则是融合掉的 Node 节点不会对剩下的节点产生影响
|
||||
|
@ -9,7 +9,7 @@
|
||||
传统编译器在编译期间,编译器会设法识别出常量表达式,对其进行求值,然后用求值的结果来替换表达式,从而使得运行时更精简。
|
||||
|
||||
```python
|
||||
day_ sec = 24*60*60
|
||||
day_sec = 24*60*60
|
||||
```
|
||||
|
||||
当编译器遇到这样的一个常量表达式时,表达式会被计算值所替换。因此上述表达式可以等效地被执行为 :
|
||||
@ -18,7 +18,7 @@ day_ sec = 24*60*60
|
||||
day_sec = 86400
|
||||
```
|
||||
|
||||
以 python 为例,在 python 中,使用反汇编模块 Disassembler 获取 day_ sec = 24×60×60 的 CPython 的字节码。
|
||||
以 python 为例,在 python 中,使用反汇编模块 Disassembler 获取 `day_sec = 24×60×60` 的 CPython 的字节码。
|
||||
|
||||
```python
|
||||
import dis
|
||||
@ -33,7 +33,7 @@ dis.dis("day_sec=24*60*60")
|
||||
6 RETURN_VALUE
|
||||
```
|
||||
|
||||
上述的 CPython 的字节码表明,python 在对 day_sec 赋值是直接加载计算结果 86400,相比于 3 次载入数据和两次乘法,更加地高效。
|
||||
上述的 CPython 的字节码表明,python 在对 `day_sec` 赋值是直接加载计算结果 86400,相比于 3 次载入数据和两次乘法,更加地高效。
|
||||
|
||||
表达式可进行常量折叠,当且仅当表达式的所有子表达式都是常量。而子表达式被判断为常量通常需要常量传播的帮助。
|
||||
|
||||
@ -77,19 +77,19 @@ int y = 0;
|
||||
return 0;
|
||||
```
|
||||
|
||||
由例子可见,常量传播对于常量折叠的重要性。在传统编译器中,常量传播主要是通过对控制流图(CFG)进行可达性分析,为每个基本块维护一个可达集合,记为 $Reaches(n)$。其含义为若定义 $d\in Reaches(n)$,则意味着存在一条从入口基本块到基本块 $b_n$ 的路径,d 没有被重新定义。计算公式如下:
|
||||
由例子可见,常量传播对于常量折叠的重要性。在传统编译器中,常量传播主要是通过对控制流图(CFG)进行可达性分析,为每个基本块维护一个可达集合,记为 $\text{Reaches}(n)$。其含义为若定义 $d\in \text{Reaches}(n)$,则意味着存在一条从入口基本块到基本块 $b_n$ 的路径,d 没有被重新定义。计算公式如下:
|
||||
|
||||
$$
|
||||
Reaches(n) = \bigcup_{m\in preds(n)}(DEDef(m)\ \cup (Reaches(m)\ \cap\ \overline{DefKill(m)}))
|
||||
\text{Reaches}(n) = \bigcup_{m\in \text{preds}(n)}(\text{DEDef}(m)\ \cup (\text{Reaches}(m)\ \cap\ \overline{\text{DefKill}(m)}))
|
||||
$$
|
||||
|
||||
方程的初始条件为:$Reaches(n) = \emptyset$, $\forall n$
|
||||
方程的初始条件为:$\text{Reaches}(n) = \emptyset$, $\forall n$
|
||||
|
||||
其中:
|
||||
|
||||
- $preds(n)$ 表示 n 的前趋结点集。
|
||||
- $DEDef(m)$ 表示基本块 $b_m$ 中向下展示的定义,其含义为若定义 $d\in DEDef(m)$,则意味着从 d 定义处到 $b_m$ 的出口处都没有被重新定义。
|
||||
- $DefKill(m)$ 表示在基本块 $b_m$ 中被杀死的定义。其含义若定义 $d\in DefKill(m)$,则意味着从 d 定义处到 $b_m$ 的出口处被重新定义。因此 $\overline{DefKill(m)}$ 包含了 m 中可见的所有定义位置。
|
||||
- $\text{preds}(n)$ 表示 n 的前趋结点集。
|
||||
- $\text{DEDef}(m)$ 表示基本块 $b_m$ 中向下展示的定义,其含义为若定义 $d\in \text{DEDef}(m)$,则意味着从 d 定义处到 $b_m$ 的出口处都没有被重新定义。
|
||||
- $\text{DefKill}(m)$ 表示在基本块 $b_m$ 中被杀死的定义。其含义若定义 $d\in \text{DefKill}(m)$,则意味着从 d 定义处到 $b_m$ 的出口处被重新定义。因此 $\overline{\text{DefKill}(m)}$ 包含了 m 中可见的所有定义位置。
|
||||
|
||||
从公式上看,如果定义 d 在基本块的出口处是可达的,当且仅当定义 d 是基本块中向下展示的定义,或者定义 d 在基本块的入口处是可定义的,并且在基本块内没有被杀死。根据入口可达集合的定义,存在一条路径即可,所以定义 d 在基本块的入口处是可达的,只需要在其任意前趋结点的出口处是可达的即可。
|
||||
|
||||
@ -106,7 +106,7 @@ String s1 = a + bc;
|
||||
在 java 中,只有被 **final** 修饰的变量,且该变量初始化的值是编译期常量,才是编译期常量。上述例子中,s1 不会被常量折叠成“abc”,而是根据 String 类特有的 **+** 运算符重载,变成类似下面这样的代码,不会进行常量折叠:
|
||||
|
||||
```java
|
||||
String s2 = new StringBuilder(a).append(b).toString();
|
||||
String s1 = new StringBuilder(a).append(b).toString();
|
||||
```
|
||||
|
||||
所以了解编译器对于编译期常量的定义是一件很重要的事情,这将决定你的代码能否进行常量折叠优化。除此之外,还有其他的一些情况可能会影响常量折叠,比如在 python 中,下面两种情况就不会被常量折叠:
|
||||
@ -188,7 +188,7 @@ String s2 = new StringBuilder(a).append(b).toString();
|
||||
|
||||
不同编译器的常量折叠的实现细节不尽相同,下面以 python 为例来描述传统编译器的常量折叠的一种实现。
|
||||
|
||||
在 python 中,CPython 会调用 astfold_expr 来对表达式进行常量折叠。astfold_expr 以递归的方式遍历 AST(抽象语法树),并尝试折叠所有的表达式。比如二值运算操作,astfold_expr 会先递归处理该二值操作的左操作数和右操作数,然后将此折叠操作代理给特定的折叠函数 fold_binop。在 fold_binop 中,首先会判断左操作数和右操作数是否都是常量,如果为常量,则判断该二值操作的具体操作类型,然后调用对应基本运算操作,比如 ADD 运算,会调用 PyNumber_Add。最后将计算出来的结果更新到 AST 对应的节点中。
|
||||
在 python 中,CPython 会调用 `astfold_expr` 来对表达式进行常量折叠。`astfold_expr` 以递归的方式遍历 AST(抽象语法树),并尝试折叠所有的表达式。比如二值运算操作,`astfold_expr` 会先递归处理该二值操作的左操作数和右操作数,然后将此折叠操作代理给特定的折叠函数 `fold_binop`。在 `fold_binop` 中,首先会判断左操作数和右操作数是否都是常量,如果为常量,则判断该二值操作的具体操作类型,然后调用对应基本运算操作,比如 ADD 运算,会调用 `PyNumber_Add`。最后将计算出来的结果更新到 AST 对应的节点中。
|
||||
|
||||
### AI 编译器实现案例
|
||||
|
||||
|
@ -36,17 +36,17 @@ d = temp + e
|
||||
|
||||
局部值编号(Local Value Numbering, LVN)是一种局部公共子表达式优化算法,其主要目的就是为了识别并消除基本块中冗余的表达式。
|
||||
|
||||
LVN 会为每个基本块都维护一个散列表,用于存储该基本块中的变量,常量以及表达式的散列值。假设表达式形如 $ x\ op\ y$,散列值用 $VN()$ 表示,LVN 计算过程如下:
|
||||
LVN 会为每个基本块都维护一个散列表,用于存储该基本块中的变量,常量以及表达式的散列值。假设表达式形如 $ x\ \text{op}\ y$,散列值用 $\text{VN}()$ 表示,LVN 计算过程如下:
|
||||
|
||||
1. 按顺序遍历所有的表达式。
|
||||
2. 分析表达式 $ x\ op\ y$ 的两个子表达式 x 和 y。查询散列表,若能查询到,则返回其所对应的散列值。若没查询到则创建一个新的表项以及散列值,插入到散列表中,并将新的散列值返回。
|
||||
3. 获得 $VN(x)$ 和 $VN(y)$ 后,计算 $VN(\{VN(x)\ op\ VN(y)\})$,若能在散列表中找到该表达式对应的散列值,则说明该表达式在前面定义过了,可以将其替换成此前该表达式的计算值。如果没找到,则在生成一个新的表项以及对应的散列值,插入到散列表中。
|
||||
2. 分析表达式 $ x\ \text{op}\ y$ 的两个子表达式 x 和 y。查询散列表,若能查询到,则返回其所对应的散列值。若没查询到则创建一个新的表项以及散列值,插入到散列表中,并将新的散列值返回。
|
||||
3. 获得 $\text{VN}(x)$ 和 $\text{VN}(y)$ 后,计算 $\text{VN}(\{\text{VN}(x)\ \text{op}\ \text{VN}(y)\})$,若能在散列表中找到该表达式对应的散列值,则说明该表达式在前面定义过了,可以将其替换成此前该表达式的计算值。如果没找到,则在生成一个新的表项以及对应的散列值,插入到散列表中。
|
||||
|
||||
### 缓式代码移动
|
||||
|
||||
缓式代码移动(Lazy Code Motion, LCM)使用一种数据流分析技术,通过可用表达式,可预测表达式以及延迟分析这三种数据流问题的方程,实现全局的公共子表达式消除。
|
||||
|
||||
在缓式代码移动中,对于某个待优化的表达式 e,通常会生成一个新的表达式赋值,将 e 赋值到一个临时变量中,形如 $h = x\ op\ y$, 其中 h 为临时变量,$x\ op\ y$ 是待优化的表达式 e。
|
||||
在缓式代码移动中,对于某个待优化的表达式 e,通常会生成一个新的表达式赋值,将 e 赋值到一个临时变量中,形如 $h = x\ \text{op}\ y$, 其中 h 为临时变量,$x\ \text{op}\ y$ 是待优化的表达式 e。
|
||||
|
||||
LCM 的算法实现主要分成两步:
|
||||
|
||||
@ -55,107 +55,107 @@ LCM 的算法实现主要分成两步:
|
||||
|
||||
#### 可用表达式
|
||||
|
||||
对于某个使用表达式`e: x op y`的操作`d`,如果在 d 上,e 是可用表达式当且仅当从程序入口到达操作 d 的所有路径都计算过 e,且从求值处到操作 d 之间,e 的任何一个子表达式的值都没有发生改变。
|
||||
对于某个使用表达式 `e: x op y` 的操作 `d`,如果在 d 上,e 是可用表达式当且仅当从程序入口到达操作 d 的所有路径都计算过 e,且从求值处到操作 d 之间,e 的任何一个子表达式的值都没有发生改变。
|
||||
|
||||
当一个表达式对于某个基本块是可用表达式,其含义为从入口基本块 $b_0$ 到基本块 $b_n$ 的入口处的所有路径都计算过 e,且从求值处到 $b_n$ 的入口处,表达式 e 没有被杀死,即其任何一个子表达式都没有被重新计算过。
|
||||
|
||||
在控制流图中,编译器为每个基本块都维护一个可用表达式集合,记为 Availn(n)。定义 Availn(n)集合的公式如下:
|
||||
在控制流图中,编译器为每个基本块都维护一个可用表达式集合,记为 Availn(n)。定义 Availn(n) 集合的公式如下:
|
||||
|
||||
$$
|
||||
AvailIn(n) = \bigcap_{m\in{preds(n)}}(DEExpr(m) \cup (AvailIn(m) \cap \overline{ExprKill(m)}))
|
||||
\text{AvailIn}(n) = \bigcap_{m\in{\text{preds}(n)}}(\text{DEExpr}(m) \cup (\text{AvailIn}(m) \cap \overline{\text{ExprKill}(m)}))
|
||||
$$
|
||||
|
||||
公式的初始条件为:$b_0$ 为程序的入口节点,$AvailIn(n_0) = \empty , AvailIn(n_i) = \{all\ expression\},\forall{n_i} \ne n_0 $
|
||||
公式的初始条件为:$b_0$ 为程序的入口节点,$\text{AvailIn}(n_0) = \empty , \text{AvailIn}(n_i) = \{\text{all}\ \text{expression}\},\forall{n_i} \ne n_0 $
|
||||
|
||||
其中:
|
||||
|
||||
- preds(n) 表示基本块 $b_n$ 所对应的前继节点集。
|
||||
- DEExpr(m) 表示基本块 $b_m$ 中向下展示的表达式,即若表达式 $e\in DEExpr$,则从表达式 e 的计算处到 $b_m$ 基本块出口都未被重新赋值过。
|
||||
- $\overline{ExprKill(m)}$ 表示在基本块 $b_m$ 中未被杀死的表达式,即当一个表达式 $e\in ExprKill(m)$,则表明该表达式存在一个或则多个子表达式在 $b_m$ 中被重新计算了。
|
||||
- $\overline{\text{ExprKill}(m)}$ 表示在基本块 $b_m$ 中未被杀死的表达式,即当一个表达式 $e\in \text{ExprKill}(m)$,则表明该表达式存在一个或则多个子表达式在 $b_m$ 中被重新计算了。
|
||||
|
||||
根据 $AvailIn(n)$ 的计算公式可知,若表达式 $e\in AvailIn(n)$ 当且仅当 e 是 $b_n$ 前继节点中向下展示的表达式或 e 在 $b_n$ 前继节点的入口和出口处都是可用表达式。
|
||||
根据 $\text{AvailIn}(n)$ 的计算公式可知,若表达式 $e\in \text{AvailIn}(n)$ 当且仅当 e 是 $b_n$ 前继节点中向下展示的表达式或 e 在 $b_n$ 前继节点的入口和出口处都是可用表达式。
|
||||
|
||||
#### 可预测表达式
|
||||
|
||||
可预测表达式通常是针对基本块来说的,编译器会为每个基本块都维护一个可预测表达式集合,记为 $AntOut(n)$。其含义为若表达式 $e\in AntOut(n)$,则表示在 $b_n$ 中出口处 e 的值可用于预测从 $b_n$ 到出口节点的所有路径上所有表达式 e 的使用,即路径上所有使用的表达式 e 都是冗余的表达式。其计算公式如下:
|
||||
可预测表达式通常是针对基本块来说的,编译器会为每个基本块都维护一个可预测表达式集合,记为 $\text{AntOut}(n)$。其含义为若表达式 $e\in \text{AntOut}(n)$,则表示在 $b_n$ 中出口处 e 的值可用于预测从 $b_n$ 到出口节点的所有路径上所有表达式 e 的使用,即路径上所有使用的表达式 e 都是冗余的表达式。其计算公式如下:
|
||||
|
||||
$$
|
||||
AntOut(n) = \bigcap_{m\in{succ(n)}}(UEExpr(m) \cup (AntOut(m) \cap \overline{ExprKill(m)}))
|
||||
\text{AntOut}(n) = \bigcap_{m\in{\text{succ}(n)}}(\text{UEExpr}(m) \cup (\text{AntOut}(m) \cap \overline{\text{ExprKill}(m)}))
|
||||
$$
|
||||
|
||||
公式的初始条件为:$b_f$ 为程序的出口节点,$AvailIn(n_f) = \empty , AvailIn(n_i) = \{all\ expression\},\forall{n_i} \ne n_0 $
|
||||
公式的初始条件为:$b_f$ 为程序的出口节点,$\text{AvailIn}(n_f) = \empty , \text{AvailIn}(n_i) = \{\text{all}\ \text{expression}\},\forall{n_i} \ne n_0 $
|
||||
|
||||
其中:
|
||||
|
||||
- succ(n) 表示基本块 $b_n$ 的后继节点块。
|
||||
- UEExpr(m) 表示向上展示的表达式,即在基本块 $b_m$ 中被杀死前所使用的表达式,即若表达式 $e\in UEExpr(m)$,如果在 $b_m$ 将对 e 的一个或多个子表达式进行修改,则这些修改操作位于使用 e 之后。
|
||||
- $\overline{ExprKill(m)}$ 表示在基本块 $b_m$ 中未被杀死的表达式,即当一个表达式 $e\in ExprKill(m)$,则表明该表达式存在一个或则多个子表达式在 $b_m$ 中被重新计算了。
|
||||
- UEExpr(m) 表示向上展示的表达式,即在基本块 $b_m$ 中被杀死前所使用的表达式,即若表达式 $e\in \text{UEExpr}\text(m)$,如果在 $b_m$ 将对 e 的一个或多个子表达式进行修改,则这些修改操作位于使用 e 之后。
|
||||
- $\overline{\text{ExprKill}(m)}$ 表示在基本块 $b_m$ 中未被杀死的表达式,即当一个表达式 $e\in \text{ExprKill}(m)$,则表明该表达式存在一个或则多个子表达式在 $b_m$ 中被重新计算了。
|
||||
|
||||
根据 $AntOut(n)$ 的计算公式可知,若表达式 $e\in AntOut(n)$ 当且仅当 e 是 $b_n$ 前继节点中向上展示的表达式或 e 在 $b_m$ 后继继节点的入口和出口处都是可预测表达式。
|
||||
根据 $\text{AntOut}(n)$ 的计算公式可知,若表达式 $e\in \text{AntOut}(n)$ 当且仅当 e 是 $b_n$ 前继节点中向上展示的表达式或 e 在 $b_m$ 后继继节点的入口和出口处都是可预测表达式。
|
||||
|
||||
#### 最早放置
|
||||
|
||||
由于进行最早放置需要使用基本块出入口的可用表达式和可预测表达式,而之前的两种数据流问题分析不够完全,下面分别给出补全的定义。
|
||||
|
||||
对于可用表达式,当已知基本块 $b_n$ 的入口可用表达式为 $AvailIn(n)$,则其出口可用表达式包含 $b_n$ 中向下展示的表达式集合以及在入口处为可用的表达式,并且在 $b_n$ 中没有被杀死的表达式集合。其定义为:
|
||||
对于可用表达式,当已知基本块 $b_n$ 的入口可用表达式为 $\text{AvailIn}(n)$,则其出口可用表达式包含 $b_n$ 中向下展示的表达式集合以及在入口处为可用的表达式,并且在 $b_n$ 中没有被杀死的表达式集合。其定义为:
|
||||
|
||||
$$
|
||||
AvailOut(n) = DEExpr(n) \cup (AvailIn(n) \cap \overline{ExprKill(n)})
|
||||
\text{AvailOut}(n) = \text{DEExpr}(n) \cup (\text{AvailIn}(n) \cap \overline{\text{ExprKill}(n)})
|
||||
$$
|
||||
|
||||
对于可预测表达式,当已知基本块 $b_n$ 的出口可预测表达式为 $AntOut(n)$,则其入口可预测表达式包含 $b_n$ 中向上展示的表达式集合以及在出口处是可预测表达式,并且在 $b_n$ 中没有被杀死的表达式集合。其定义为:
|
||||
对于可预测表达式,当已知基本块 $b_n$ 的出口可预测表达式为 $\text{AntOut}(n)$,则其入口可预测表达式包含 $b_n$ 中向上展示的表达式集合以及在出口处是可预测表达式,并且在 $b_n$ 中没有被杀死的表达式集合。其定义为:
|
||||
|
||||
$$
|
||||
AntIn(n) = UEExpr(n) \cup (AntOut(n) \cap \overline{ExprKill(n)})
|
||||
\text{AntIn}(n) = \text{UEExpr}(n) \cup (\text{AntOut}(n) \cap \overline{\text{ExprKill}(n)})
|
||||
$$
|
||||
|
||||
为了简化最早放置的过程分析,假设每条边含有一个存储集,记为 Earliest(i,j),其中 i 和 j 分别表示源节点和目的节点的编号,用于存储待优化的表达式。其含义为若表达式 $e\in Earliest(i,j)$,则该表达式无法无法通过基本块 $b_i$ 前往更早的边,即 $b_i$ 为该表达式的最早赋值边界。其定义为:
|
||||
为了简化最早放置的过程分析,假设每条边含有一个存储集,记为 $\text{Earliest}(i,j)$,其中 i 和 j 分别表示源节点和目的节点的编号,用于存储待优化的表达式。其含义为若表达式 $e\in \text{Earliest}(i,j)$,则该表达式无法无法通过基本块 $b_i$ 前往更早的边,即 $b_i$ 为该表达式的最早赋值边界。其定义为:
|
||||
|
||||
$$
|
||||
Earliest(i,j) = AntIn(j)\ \cap\ \overline{AvailOut(i)}\ \cap\ (ExprKill(i)\ \cup \overline{AntOut(i)})
|
||||
\text{Earliest}(i,j) = \text{AntIn}(j)\ \cap\ \overline{AvailOut(i)}\ \cap\ (\text{ExprKill}(i)\ \cup \overline{\text{AntOut}(i)})
|
||||
$$
|
||||
|
||||
其中:
|
||||
|
||||
- 若表达式 $e\in AntIn(j)$,根据入口可预测表达式的定义,从 $b_j$ 的入口处到出口基本块,表达式 e 的任何一个子表达式都没有被重新定值,即可以安全的将表达式 e 移动到 $Earliest(i,j)$ 中,这里的安全性表示将表达式 e 上移后,不会使其他不包含该表达式的路径引入该表达式,即不会产生新的冗余表达式。
|
||||
- 若表达式 $e\in \text{AntIn}(j)$,根据入口可预测表达式的定义,从 $b_j$ 的入口处到出口基本块,表达式 e 的任何一个子表达式都没有被重新定值,即可以安全的将表达式 e 移动到 $\text{Earliest}(i,j)$ 中,这里的安全性表示将表达式 e 上移后,不会使其他不包含该表达式的路径引入该表达式,即不会产生新的冗余表达式。
|
||||
- 若表达式 $e\in \overline{AvailOut(i)}$,根据出口可用表达式的定义,这意味着从入口基本块到 $b_i$ 的路径上,e 的一个或多个子表达式被重新定值。
|
||||
- 根据第二个条件,为了使得表达式 e 不能通过 $b_i$ 前往更早的边,则 e 的子表达式重新定值需要发生在 $b_i$ 中,所以此时表达式 $e\in ExprKill(i)$。如果表达式 $e\in \overline{AntOut(i)}$,根据出口可预测表达式的定义,这意味着此时存在一个或者多个 $b_i$ 的后继节点,表达式 e 在这些基本块的入口处不是可预测的,则说明表达式 e 的一个或多个子表达式在这些路径中被重新定值,此时表达式 e 甚至无法被移动到 $b_i$ 中,就更不可能移动到更早的边上。这两个条件除非同时不满足,否则表达式不能移动到更早的边。
|
||||
- 根据第二个条件,为了使得表达式 e 不能通过 $b_i$ 前往更早的边,则 e 的子表达式重新定值需要发生在 $b_i$ 中,所以此时表达式 $e\in \text{ExprKill}(i)$。如果表达式 $e\in \overline{\text{AntOut}(i)}$,根据出口可预测表达式的定义,这意味着此时存在一个或者多个 $b_i$ 的后继节点,表达式 e 在这些基本块的入口处不是可预测的,则说明表达式 e 的一个或多个子表达式在这些路径中被重新定值,此时表达式 e 甚至无法被移动到 $b_i$ 中,就更不可能移动到更早的边上。这两个条件除非同时不满足,否则表达式不能移动到更早的边。
|
||||
|
||||
#### 延迟放置
|
||||
|
||||
在完成最早放置后,编译器会进行延迟分析。延迟分析是控制流图上的一个前向数据流问题,其主要目的是为了判断边上的某个待优化表达式能否通过其目的节点,进入到下一条边中。
|
||||
|
||||
编译器为每个基本块维护一个集合 $LaterIn(n)$,其含义为若表达式 $e\in LaterIn(n)$,则表达式 e 在 $b_n$ 的入口处是可延迟的。为每条边维护一个集合 $Later(i,j)$,其含义为若表达式 $e\in Later(i,j)$,则表示表达式 e 可以进入到 $b_j$ 中,直观上看 $Later(i,j)$ 就是个可能下沉表达式集合。其定义如下:
|
||||
编译器为每个基本块维护一个集合 $\text{LaterIn}(n)$,其含义为若表达式 $e\in \text{LaterIn}(n)$,则表达式 e 在 $b_n$ 的入口处是可延迟的。为每条边维护一个集合 $\text{Later}(i,j)$,其含义为若表达式 $e\in \text{Later}(i,j)$,则表示表达式 e 可以进入到 $b_j$ 中,直观上看 $\text{Later}(i,j)$ 就是个可能下沉表达式集合。其定义如下:
|
||||
|
||||
$$
|
||||
LaterIn(j) = \bigcap_{i\in preds(j)}\ Later(i,j), j\ne n_0 \\
|
||||
Later(i,j) = Earliest(i,j)\ \cup\ (LaterIn(i)\ \cap\ \overline{UEExpr(i)}),\ i\in preds(j)
|
||||
\text{LaterIn}(j) = \bigcap_{i\in preds(j)}\ \text{Later}(i,j), j\ne n_0 \\
|
||||
\text{Later}(i,j) = \text{Earliest}(i,j)\ \cup\ (\text{LaterIn}(i)\ \cap\ \overline{\text{UEExpr}(i)}),\ i\in \text{preds}(j)
|
||||
$$
|
||||
|
||||
从公式上看,可能下沉表达式集合分为两部分。若表达式 $e\in Earliest(i,j)$,则表达式 e 一定是可能下沉的表达式,而另一部分的表达式来自于 从 $b_i$ 下沉下来的表达式,这些表达式满足在 $b_i$ 的入口处是可下沉的,并且这些表达式在 $b_i$ 不是向上展示的表达式,因为如果表达式 $e\in UEExpr(i)$,则表明表达式在 $b_i$ 中被求值,如果下沉,那么下沉的这个表达式则成为冗余表达式。
|
||||
从公式上看,可能下沉表达式集合分为两部分。若表达式 $e\in \text{Earliest}(i,j)$,则表达式 e 一定是可能下沉的表达式,而另一部分的表达式来自于 从 $b_i$ 下沉下来的表达式,这些表达式满足在 $b_i$ 的入口处是可下沉的,并且这些表达式在 $b_i$ 不是向上展示的表达式,因为如果表达式 $e\in \text{UEExpr}(i)$,则表明表达式在 $b_i$ 中被求值,如果下沉,那么下沉的这个表达式则成为冗余表达式。
|
||||
|
||||
#### 重写代码
|
||||
|
||||
最后一步使用 $LaterIn(n)$ 和 $Later(i,j)$ 生成额外的集合指导编译器重写代码,额外集合包括 $Insert(i,j)$ 和 $Delete(i)$ 集合。
|
||||
最后一步使用 $\text{LaterIn}(n)$ 和 $\text{Later}(i,j)$ 生成额外的集合指导编译器重写代码,额外集合包括 $\text{Insert}(i,j)$ 和 $\text{Delete}(i)$ 集合。
|
||||
|
||||
$Insert(i,j)$ 表示可插入的集合。其定义如下:
|
||||
$\text{Insert}(i,j)$ 表示可插入的集合。其定义如下:
|
||||
|
||||
$$
|
||||
Insert(i,j) = Later(i,j)\ -\ (LaterIn(j)\ \cap \overline{UEExpr(j)})
|
||||
\text{Insert}(i,j) = \text{Later}(i,j)\ -\ (\text{LaterIn}(j)\ \cap \overline{\text{UEExpr}(j)})
|
||||
$$
|
||||
|
||||
从公式上看,$Later(i,j)$ 为可能下沉的集合,$LaterIn(j)\ \cap \overline{UEExpr(j)}$ 表示可以从 $b_j$ 下沉到底部的集合。所以去掉可能下沉集合中的所有能下沉的表达式,留下的就是不能下沉的表达式,即需要插入的表达式。
|
||||
从公式上看,$\text{Later}(i,j)$ 为可能下沉的集合,$\text{LaterIn}(j)\ \cap \overline{UEExpr(j)}$ 表示可以从 $b_j$ 下沉到底部的集合。所以去掉可能下沉集合中的所有能下沉的表达式,留下的就是不能下沉的表达式,即需要插入的表达式。
|
||||
|
||||
插入规则为:
|
||||
|
||||
- 如果 $b_i$ 只有一个后继节点 $b_j$,则将 $Insert(i,j)$ 中的表达式插入到 $b_i$ 的出口处。
|
||||
- 如果 $b_j$ 只有一个前趋节点 $b_i$,则将 $Insert(i,j)$ 中的表达式插入到 $b_j$ 的入口处。
|
||||
- 若前两个条件都不满足,则在边 ${b_i\rightarrow b_j}$ 上新建一个基本块,将 $Insert(i,j)$ 中的表达式插入到该基本块中。
|
||||
- 如果 $b_i$ 只有一个后继节点 $b_j$,则将 $\text{Insert}(i,j)$ 中的表达式插入到 $b_i$ 的出口处。
|
||||
- 如果 $b_j$ 只有一个前趋节点 $b_i$,则将 $\text{Insert}(i,j)$ 中的表达式插入到 $b_j$ 的入口处。
|
||||
- 若前两个条件都不满足,则在边 ${b_i\rightarrow b_j}$ 上新建一个基本块,将 $\text{Insert}(i,j)$ 中的表达式插入到该基本块中。
|
||||
|
||||
需要注意的是,由于插入的是新的赋值语句,所以插入后,有一些表达式冗余了。比如对于一个基本块的向上展示的表达式。因此编译器对每个基本块都需要维护一个删除集合,记为 $Delete(i)$。其定义如下:
|
||||
需要注意的是,由于插入的是新的赋值语句,所以插入后,有一些表达式冗余了。比如对于一个基本块的向上展示的表达式。因此编译器对每个基本块都需要维护一个删除集合,记为 $\text{Delete}(i)$。其定义如下:
|
||||
|
||||
$$
|
||||
Delete(j) = UEExpr(j)\ \cap \ \bigcup_{i\in preds(j)} Insert(i,j)
|
||||
\text{Delete}(j) = \text{UEExpr}(j)\ \cap \ \bigcup_{i\in \text{preds}(j)} \text{Insert}(i,j)
|
||||
$$
|
||||
|
||||
## AI 编译器的公共表达式消除
|
||||
@ -166,7 +166,7 @@ AI 编译器中公共子表达式消除采取相同的思路,区别在于 AI
|
||||
|
||||

|
||||
|
||||
图中 Op3 和 Op4 都经过了相同的图结构$\{\{Op1,Op2\},Op1\rightarrow Op2\}$, AI 编译器会将相同子图的所有不同输出都连接到同一个子图上,然后会在后续的死代码消除中删除其他相同的子图,从而达到简化计算图的目的。减少计算开销。
|
||||
图中 Op3 和 Op4 都经过了相同的图结构$\{\{ \text{Op1}, \text{Op2} \}, \text{Op1}\rightarrow \text{Op2}\}$, AI 编译器会将相同子图的所有不同输出都连接到同一个子图上,然后会在后续的死代码消除中删除其他相同的子图,从而达到简化计算图的目的。减少计算开销。
|
||||
|
||||
## 公共子表达式实现案例
|
||||
|
||||
@ -176,13 +176,13 @@ AI 编译器中公共子表达式消除采取相同的思路,区别在于 AI
|
||||
|
||||
支配性的含义是对于入口基本块 $b_0$ 以及任意基本块 $b_i$ 和 $b_j$,其中 $i\ne j$。如果从 $b_0$ 到 $b_i$ 的所有路径都经过 $b_j$ ,则称 $b_j$ 是 $b_i$ 的支配节点。
|
||||
|
||||
支配性是一个正向数据流问题。编译器会为每个基本块维护一个支配节点集合,记为 $Dom(n)$。计算公式如下:
|
||||
支配性是一个正向数据流问题。编译器会为每个基本块维护一个支配节点集合,记为 $\text{Dom}(n)$。计算公式如下:
|
||||
|
||||
$$
|
||||
Dom(n) = {n}\ \cup \ (\bigcap_{m\in preds(n)}Dom(m))
|
||||
\text{Dom}(n) = {n}\ \cup \ (\bigcap_{m\in \text{preds}(n)}\text{Dom}(m))
|
||||
$$
|
||||
|
||||
初始条件为:$Dom(n_0) = n_0,\ Dom(n_i) = N$, 其中 N 是控制流图中所有节点的集合,$i\ne 0$。公式中的 $preds(n)$ 表示 $b_n$ 的前继节点集合。
|
||||
初始条件为:$\text{Dom}(n_0) = n_0,\ \text{Dom}(n_i) = N$, 其中 N 是控制流图中所有节点的集合,$i\ne 0$。公式中的 $\text{preds}(n)$ 表示 $b_n$ 的前继节点集合。
|
||||
|
||||
#### 静态单一赋值(SSA)
|
||||
|
||||
|
@ -106,7 +106,7 @@ test(1,2,3)
|
||||
后支配性:在控制流图中,对于基本块 $b_i$ 和 $b_j$,如果所有非空路径从 $b_i$ 到出口基本块都经过 $b_j$,则称 $b_j$ 后支配 $b_i$。其计算公式为
|
||||
|
||||
$$
|
||||
RDOM(n) = \{n\} \cup (\bigcap_{m\in{succ(n)}}RDOM(m))
|
||||
\text{RDOM}(n) = \{n\} \cup (\bigcap_{m\in{\text{succ}(n)}}\text{RDOM}(m))
|
||||
$$
|
||||
|
||||
其中 RDOM(n) 表示 基本块 $b_n$ 所对应的后支配节点,succ(n)表示 基本块 $b_n$ 的所有后继节点。在此处并不考虑支配性的严格性,所以 RDOM(n) 中包含自身。而对于后继节点的后支配节点,由后支配性的定义,需要所有路径同时经过的基本块,所以这里取所有后继节点的后支配节点的交集。
|
||||
@ -115,13 +115,13 @@ $$
|
||||
|
||||
如上图,其中 $b_3$ 的后支配节点为 {$b_3$,$b_6$,$b_7$,$b_8$},$b_4$ 的后支配节点为 {$b_4$,$b_6$,$b_7$,$b_8$},其余节点的后支配节点读者可以自行探索。
|
||||
|
||||
控制依赖性:在控制流图中,对于基本块 $b_i$ 和 $b_j$,如果存在一条或多条非空路径从 $b_i$ 到 出口基本块,且路径经过 $b_j$ 。同时也存在一条或多条非空路径从 $b_i$ 到 出口基本块,且路径不经过 $b_j$,直观上看,$b_j$ 能否执行取决于 $b_i$ 的条件转移,$b_j$ 在控制上依赖 $b_i$,称 $b_i$ 是 $b_j$ 的控制依赖点。
|
||||
控制依赖性:在控制流图中,对于基本块 $b_i$ 和 $b_j$,如果存在一条或多条非空路径从 $b_i$ 到出口基本块,且路径经过 $b_j$ 。同时也存在一条或多条非空路径从 $b_i$ 到出口基本块,且路径不经过 $b_j$,直观上看,$b_j$ 能否执行取决于 $b_i$ 的条件转移,$b_j$ 在控制上依赖 $b_i$,称 $b_i$ 是 $b_j$ 的控制依赖点。
|
||||
|
||||

|
||||
|
||||
如上图,$B_4$ 和 $B_5$ 的控制依赖点为 $B_3$ ,以 $B_4$ 为例,从 $B_3$ 到出口节点 $B_8$,存在非空路径$\{B_3\rightarrow B_4\rightarrow B_6\rightarrow B_8\}$经过 $B_4$,又存在非空路径$\{B_3\rightarrow B_5\rightarrow B_6\rightarrow B_8\}$不经过 $B_4$,所以 $B_4$ 的控制依赖点为 $B_3$ , $B_5$ 同理。
|
||||
|
||||
在传统编译器中,对于某个关键操作 i ,假设 i:v = x op y,如果操作 x 和 y 的定义没有被标记上有用,则将其标记为有用操作。然后遍历关机操作 d 所在块的控制依赖块,将控制依赖块的条件转移操作标记为有用。在标记操作完成后,需要将无用的条件转移操作转化为跳转操作,跳转到该基本块的第一个含有用操作的后支配节点。因为当一个基本块的条件转移操作没有被标记为有用,那么从其后继结点一直到其直接的后向支配者结点,都不包含有用操作。如果其直接后继节点不包含有用的操作,也是同样。由于出口节点是一定含有有用操作,所以上述操作在向后查找的过程中,一定会停止。
|
||||
在传统编译器中,对于某个关键操作 i ,假设 `i:v = x op y`,如果操作 x 和 y 的定义没有被标记上有用,则将其标记为有用操作。然后遍历关机操作 d 所在块的控制依赖块,将控制依赖块的条件转移操作标记为有用。在标记操作完成后,需要将无用的条件转移操作转化为跳转操作,跳转到该基本块的第一个含有用操作的后支配节点。因为当一个基本块的条件转移操作没有被标记为有用,那么从其后继结点一直到其直接的后向支配者结点,都不包含有用操作。如果其直接后继节点不包含有用的操作,也是同样。由于出口节点是一定含有有用操作,所以上述操作在向后查找的过程中,一定会停止。
|
||||
|
||||
## AI 编译器中的死代码消除
|
||||
|
||||
|
@ -43,11 +43,11 @@ $$
|
||||
当然还有许多符合结合律的化简,我们列几个在下方供读者参考。
|
||||
|
||||
$$
|
||||
Recip(A) \diamond Recipe(A \diamond B) \rightarrow Square(Recip(A)) \diamond B
|
||||
\text{Recip}(A) \diamond \text{Recipe}(A \diamond B) \rightarrow \text{Square}(\text{Recip}(A)) \diamond B
|
||||
\\
|
||||
(A \diamond \sqrt B) \diamond (\sqrt B \diamond C) \rightarrow A \diamond B \diamond C
|
||||
\\
|
||||
(A \diamond ReduceSum(B)) \diamond (ReduceSum(B) \diamond C) \rightarrow A Square(ReduceSum(B)) \diamond C
|
||||
(A \diamond \text{ReduceSum}(B)) \diamond (\text{ReduceSum}(B) \diamond C) \rightarrow A \text{Square}(\text{ReduceSum}(B)) \diamond C
|
||||
$$
|
||||
|
||||
### 交换律
|
||||
@ -70,7 +70,7 @@ $$
|
||||
根据这样简洁优美的思想,我们可以发现以下的规则符合结合律:
|
||||
|
||||
$$
|
||||
ReduceSum(BitShift(A)) \rightarrow BitShift(ReduceSum(A))
|
||||
\text{ReduceSum}(\text{BitShift}(A)) \rightarrow \text{BitShift}(\text{ReduceSum}(A))
|
||||
$$
|
||||
|
||||
根据这样的规则我们可以看到如下实例的优化:
|
||||
@ -82,7 +82,7 @@ $$
|
||||
当然还有许多符合交换律的化简,我们列几个在下方供读者参考。
|
||||
|
||||
$$
|
||||
ReduceProd(Exp(A)) \rightarrow Exp(ReduceSum(A))
|
||||
\text{ReduceProd}(\text{Exp}(A)) \rightarrow \text{Exp}(\text{ReduceSum}(A))
|
||||
$$
|
||||
|
||||
### 分配律
|
||||
@ -120,7 +120,7 @@ $$
|
||||
$$
|
||||
A+A\diamond B \rightarrow A \diamond (B+1)
|
||||
\\
|
||||
Square(A+B)-(A+B)\diamond C \rightarrow (A+B)\diamond(A+B-C)
|
||||
\text{Square}(A+B)-(A+B)\diamond C \rightarrow (A+B)\diamond(A+B-C)
|
||||
$$
|
||||
|
||||
> 注:当我们做代数简化时,一定要先注意到算子是否符合例如交换律,结合律等规则,例如矩阵乘法中 $AB \neq BA$。
|
||||
@ -155,7 +155,7 @@ $$
|
||||
一个具体的实例如下:
|
||||
|
||||
$$
|
||||
Reshape(Reshape(x, shape1),shape2) \rightarrow Reshape(x, shape2)
|
||||
\text{Reshape}(\text{Reshape}(x, shape1),shape2) \rightarrow \text{Reshape}(x, shape2)
|
||||
$$
|
||||
|
||||
其中,$Shape2$ 的大小小于 $Shape1$。
|
||||
@ -177,7 +177,7 @@ $$
|
||||
我们还是以一个简单的例子为准,考虑以下 2 个矩阵与 2 个向量的相加:
|
||||
|
||||
$$
|
||||
(S_1+Mat_1)+(S_2+Mat_2) \rightarrow(S_1+S_2)+(Mat_1+Mat_2)
|
||||
(S_1+{Mat}_1)+(S_2+ {Mat}_2) \rightarrow(S_1+S_2)+(Mat_1+Mat_2)
|
||||
$$
|
||||
|
||||

|
||||
|
@ -34,6 +34,4 @@ AI 编译器主要是分为前端优化、后端优化,部分还会有中间
|
||||
>
|
||||
> 欢迎大家使用的过程中发现 bug 或者勘误直接提交代码 PR 到开源社区哦!
|
||||
>
|
||||
> 欢迎大家使用的过程中发现 bug 或者勘误直接提交 PR 到开源社区哦!
|
||||
>
|
||||
> 请大家尊重开源和 ZOMI 的努力,引用 PPT 的内容请规范转载标明出处哦!
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
文字课程内容正在一节节补充更新,尽可能抽空继续更新正在 [AISys](https://chenzomi12.github.io/) ,希望您多多鼓励和参与进来!!!
|
||||
|
||||
文字课程开源在 [AISys](https://chenzomi12.github.io/),系列视频托管[B 站](https://space.bilibili.com/517221395)和[油管](https://www.youtube.com/@zomi6222/videos),PPT 开源在[github](https://github.com/chenzomi12/AISystem),欢迎取用!!!
|
||||
文字课程开源在 [AISys](https://infrasys-ai.github.io/aisystem-docs/),系列视频托管[B 站](https://space.bilibili.com/517221395)和[油管](https://www.youtube.com/@zomi6222/videos),PPT 开源在[github](https://github.com/chenzomi12/AISystem),欢迎取用!!!
|
||||
|
||||
## 课程背景
|
||||
|
||||
@ -97,4 +97,4 @@
|
||||
>
|
||||
> 欢迎大家使用的过程中发现 bug 或者勘误直接提交代码 PR 到开源社区哦!
|
||||
>
|
||||
> 请大家尊重开源和 ZOMI 的努力,引用 PPT 的内容请规范转载标明出处哦!
|
||||
> 请大家尊重开源和 ZOMI 的努力,引用 PPT 的内容请规范转载标明出处哦!
|
||||
|
Reference in New Issue
Block a user