2. 计算图基础:计算图的概念、静态图与动态图、计算图在编译器中的作用
2.1 计算图到底是什么?
先说说我自己的理解。计算图,说白了就是把数学运算画成一张图。
你想想看,一个神经网络模型,无非就是一堆矩阵乘法、卷积、激活函数堆在一起。计算图就是把这些操作拆开,每个操作变成一个节点,数据流向变成边。嗯,就这么简单。
举个例子,一个简单的表达式 z = (a + b) × c,用计算图表示就是:
a ──┐
├──> (+) ──> (×) ──> z
b ──┘ ↑
c
每个圆圈是一个操作节点,箭头是数据流向。我在项目中遇到过不少新人,一上来就写几百行的 forward 函数,结果调试起来一头雾水。要是先画个计算图,问题早定位了。
核心要点:计算图 = 节点(算子)+ 边(数据依赖)
- 节点:加法、乘法、卷积、ReLU 等操作
- 边:张量(Tensor)在节点间流动
- 方向:从输入到输出,前向传播的方向
2.2 静态图 vs 动态图
这是面试高频题,也是实际开发中必须做的选择。我两种都写过,说说我的感受。
静态图(Define-and-Run)
先定义好整个计算图,再喂数据执行。TensorFlow 1.x 就是典型代表。
import tensorflow as tf
# 先定义图结构
a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
c = tf.constant(2.0)
z = tf.multiply(tf.add(a, b), c)
# 再创建会话执行
with tf.Session() as sess:
result = sess.run(z, feed_dict={a: 1.0, b: 2.0})
print(result) # 输出 6.0
静态图的优点很明显:
- 性能好:图结构固定,编译器可以做全局优化
- 内存可控:提前知道所有中间结果的大小
- 便于部署:导出的计算图就是完整的推理逻辑
但缺点也让人头疼:
- 调试困难:你不能在中间打断点看数据
- 灵活性差:if-else 这种控制流需要特殊处理
- 学习成本高:新手往往搞不清 placeholder 和 session
我曾经踩过的坑:在 TensorFlow 1.x 里写了一个带条件分支的网络,结果发现静态图不支持动态的 if-else。最后只能用 tf.cond 重写,折腾了一整天。所以后来我特别关注框架对控制流的支持程度。
动态图(Define-by-Run)
边定义边执行,PyTorch 就是这种风格。代码写起来就像写普通的 Python 程序。
import torch
a = torch.tensor(1.0, requires_grad=True)
b = torch.tensor(2.0, requires_grad=True)
c = torch.tensor(2.0)
z = (a + b) * c
print(z) # 输出 tensor(6.0, grad_fn=<MulBackward0>)
动态图的优势:
- 直觉友好:代码执行顺序就是计算顺序
- 调试方便:随时可以 print 中间结果
- 灵活性强:原生支持 Python 控制流
代价呢?
- 性能开销:每次执行都要重新构建图
- 优化受限:编译器看不到全局结构
- 部署复杂:需要保留完整的运行时环境
| 对比维度 | 静态图 | 动态图 |
|---|---|---|
| 构建时机 | 执行前 | 执行时 |
| 调试体验 | 差 | 好 |
| 执行性能 | 高 | 中 |
| 内存优化 | 容易 | 困难 |
| 控制流支持 | 需特殊处理 | 原生支持 |
| 典型框架 | TensorFlow 1.x, Caffe | PyTorch, TensorFlow Eager |
我的建议:做研究原型用动态图,快速迭代;做产品部署用静态图,追求性能。现在很多框架支持 JIT 编译,比如 PyTorch 的 torch.jit.trace,可以先把动态图转成静态图再优化,两全其美。
2.3 计算图在编译器中的作用
好了,现在聊聊编译器视角。计算图对编译器来说意味着什么?
我个人习惯把编译器的工作分成三步:
- 前端解析:把模型代码转成计算图 IR
- 中间优化:在计算图上做各种变换
- 后端生成:把优化后的图映射到目标硬件
计算图就是整个编译过程的「通用语言」。没有它,编译器根本没法理解你的模型。
计算图带来的优化机会
有了计算图,编译器就能做很多「人眼看不出」的优化:
- 算子融合:把 Conv + BN + ReLU 合并成一个算子,减少内存读写
- 常量折叠:提前算好那些不变的子表达式
- 死代码消除:删掉那些计算结果不会被用到的节点
- 内存规划:复用中间张量的内存空间
举个例子,算子融合的效果:
# 优化前:三个独立算子
input ──> Conv ──> BN ──> ReLU ──> output
(写内存) (读/写) (读/写)
# 优化后:融合成一个算子
input ──> FusedConvBNReLU ──> output
(只写一次内存)
我在项目中遇到过,一个简单的 Conv-BN-ReLU 融合,推理速度能提升 30% 以上。这就是计算图的价值——让编译器看到全局,做人类手工难以完成的优化。
计算图的表示形式
编译器内部怎么存计算图?常见的有三种:
- 邻接表:每个节点记录它的输入节点列表
- 拓扑排序列表:按执行顺序排列节点
- SSA 形式:静态单赋值,每个变量只赋值一次
我个人比较喜欢 SSA 形式,因为它天然适合做数据流分析。TVM 的 Relay IR 就是基于 SSA 的。
一句话总结:计算图是深度学习编译器的「中间人」。它把前端模型和后端硬件解耦,让优化变得系统化、自动化。
2.4 本章知识体系
下面这张图是我自己画的,把本章的核心逻辑串起来了:
嗯,这张图把本章的核心逻辑都串起来了。从概念出发,到两种模式的对比,再到编译器如何利用计算图做优化,一条线走下来。
一个小技巧:如果你刚开始接触计算图,建议先用 PyTorch 写几个小网络,然后用 torchviz 把计算图可视化出来。亲眼看到图的结构,比看文档理解快得多。
公众号:蓝海资料掘金营,微信deep3321