2、计算图基础:计算图的概念、节点与边的定义、数据流图与控制流图
各位同学,今天我们聊聊计算图。说实话,这个概念听起来有点唬人,但说白了,它就是芯片设计里最基础的“骨架”。我做了十几年芯片,见过太多项目因为图没画明白,最后性能崩得一塌糊涂。所以这一节,咱们把地基打牢。
2.1 计算图到底是个啥?
我个人习惯把计算图理解成一张“工作说明书”。它告诉芯片:你要做什么,先做哪个,后做哪个,数据从哪里来,结果送到哪里去。
举个例子。你写了一段代码:c = a + b; d = c * e;。在计算图里,a、b、e是输入节点,+和*是运算节点,c和d是中间结果节点。箭头从输入指向运算,再从运算指向结果。就这么简单。
嗯,这里要注意:计算图不是电路图。电路图画的是晶体管怎么连,计算图画的是数据怎么流动、操作怎么依赖。这是两个层次的东西。
核心定义:计算图是一个有向图,节点表示操作或数据,边表示依赖关系或数据流动方向。
2.2 节点与边的定义
咱们拆开来看。
节点(Node)
节点就是图中的“圆圈”或“方框”。在我做过的项目里,节点通常分三类:
- 操作节点:比如加法器、乘法器、比较器、逻辑运算。这些是真正干活的。
- 数据节点:比如常量、输入变量、中间结果。这些是“物料”。
- 控制节点:比如条件判断、循环入口。这些是“调度员”。
我曾经在一个AI加速器项目里,把整个神经网络映射成计算图。当时一个卷积层就有上千个操作节点,密密麻麻的。后来我学乖了——节点一定要分层,不然你根本看不清楚。
边(Edge)
边就是箭头。它表示“谁依赖谁”。
我个人习惯把边分成两种:
- 数据边:传递数值。比如从加法器输出到乘法器输入。
- 控制边:传递使能信号或条件。比如“只有当flag为真时,才执行这个乘法”。
你想想看,如果数据边和控制边混在一起,编译优化的时候就会出大问题。我见过有人把控制信号也当成数据流去优化,结果逻辑全乱了。
避坑指南:我曾经在写图编译器时,忘记区分数据边和控制边,导致调度器把条件分支里的操作提前执行了。嗯,从那以后,我强制要求团队在节点属性里加一个“type”字段,明确标注是data还是control。
2.3 数据流图与控制流图
这两个概念容易混淆。我简单说清楚。
数据流图(Data Flow Graph, DFG)
数据流图只关心数据怎么算。它不考虑“什么时候算”,只考虑“谁需要谁”。
举个例子:
// 代码
x = a + b;
y = c * d;
z = x + y;
对应的数据流图:
a ──┐
├─ [+] ── x ──┐
b ──┘ ├─ [+] ── z
c ──┐ │
├─ [*] ── y ──┘
d ──┘
你看,a+b和c*d是独立的,可以并行算。这就是数据流图的价值——它暴露了并行性。
我在做高性能计算芯片时,特别喜欢用数据流图来挖掘指令级并行。说白了,数据流图越“宽”,你的芯片就能塞越多的并行单元。
控制流图(Control Flow Graph, CFG)
控制流图关心的是“执行顺序”和“分支跳转”。它把程序拆成一个个基本块(Basic Block),然后用箭头表示跳转关系。
比如:
if (cond) {
x = a + b;
} else {
x = c + d;
}
y = x * 2;
控制流图是这样的:
┌──────────┐
│ 判断cond │
└────┬─────┘
┌──┴──┐
▼ ▼
┌────────┐ ┌────────┐
│ x=a+b │ │ x=c+d │
└────┬───┘ └────┬───┘
└────┬─────┘
▼
┌──────────┐
│ y=x*2 │
└──────────┘
控制流图对芯片设计的意义在于:它决定了流水线怎么处理分支预测,以及乱序执行窗口怎么管理。
注意:数据流图和控制流图不是互斥的。实际芯片里,两者要结合。我习惯的做法是:先用控制流图确定基本块,再在每个基本块内部画数据流图。这样既保证了顺序正确,又挖掘了局部并行。
2.4 一张图看懂本章核心
下面我用SVG画一张图,把计算图、节点、边、数据流、控制流的关系串起来。这张图是我自己讲课常用的,你多看几遍,后面章节会反复用到这些概念。
2.5 为什么这对芯片性能至关重要?
我直接说结论:计算图的质量,决定了编译优化的天花板。
举个例子。你在数据流图里发现两个加法没有依赖关系,编译器就可以把它们调度到不同的ALU上并行执行。如果你画的控制流图里有一个循环,编译器就可以尝试循环展开或者流水线化。
反过来,如果图没画好——比如把本应并行的操作串成了链,或者把控制依赖搞成了数据依赖——那编译器再怎么优化也救不回来。我曾经接手过一个项目,性能死活上不去,最后发现是计算图里多了一条不该有的边,导致调度器以为两个操作有依赖,白白浪费了并行机会。
我的建议:刚开始学图编译的同学,先别急着写代码。拿张纸,把你想要加速的算法画成计算图。画清楚节点和边,标出哪些是数据依赖,哪些是控制依赖。这一步做扎实了,后面写编译器就是水到渠成的事。
好了,这一节就到这里。记住:计算图是芯片设计的“第一性原理”。你把它吃透了,后面讲调度、映射、优化,你都能跟得上。
公众号:蓝海资料掘金营,微信deep3321