2. AI模型计算图
各位同学,今天我们聊聊计算图。这东西听起来有点抽象,但说白了,它就是AI模型的“骨架”。你想想看,一个神经网络少则几十层,多则上千层,层与层之间怎么连接?数据怎么流动?总得有个东西把它描述清楚吧。计算图就是干这个的。
2.1 计算图到底是什么?
我个人习惯把计算图理解成一张“数据流动的蓝图”。它用节点(Node)表示运算,用边(Edge)表示张量(Tensor)的流动方向。比如一个简单的加法 c = a + b,在计算图里就是两个输入节点指向一个加法节点,加法节点再输出c。
我在项目中遇到过最直观的例子:用TensorFlow 1.x写一个线性回归模型。那时候你必须先定义好整个计算图,然后才能启动Session去执行。就像盖房子,你得先把图纸画好,再让施工队进场。嗯,这里要注意,计算图本身不执行任何运算,它只是定义了“怎么算”。
核心要点:计算图是一种有向无环图(DAG),它把复杂的数学运算拆解成一系列基本操作,并明确记录了数据依赖关系。
为什么需要这个?因为有了图,编译器才能做优化。比如算子融合、内存复用、并行调度,这些高级操作都得在计算图的基础上进行。没有图,编译器就是个瞎子。
2.2 静态图 vs 动态图:两种哲学
这里有个经典问题:静态图和动态图到底有什么区别?我刚开始学的时候也懵,后来用TensorFlow 1.x和PyTorch各写了一个模型,才真正搞明白。
| 对比维度 | 静态图(TensorFlow 1.x) | 动态图(PyTorch) |
|---|---|---|
| 构建时机 | 先定义图,再执行 | 边定义边执行 |
| 调试难度 | 较高,无法在图中打断点 | 较低,Python原生调试 |
| 性能优化 | 容易做全局优化 | 优化空间有限 |
| 灵活性 | 差,控制流需特殊处理 | 强,支持任意Python控制流 |
| 典型框架 | TensorFlow 1.x, Theano | PyTorch, TensorFlow Eager |
静态图就像拍电影——先写好剧本(定义图),再按剧本演(执行)。动态图则像即兴表演——想到哪演到哪,灵活但不好控制全局。
我的经验:如果你在做研究,经常要改模型结构,用动态图(PyTorch)会舒服很多。如果是做产品部署,追求极致性能,静态图(TensorFlow 2.x的@tf.function或ONNX)更合适。
2.3 TensorFlow的计算图表示
TensorFlow 1.x是静态图的典型代表。我记得第一次写TF代码时,被Session和placeholder搞得晕头转向。来看个例子:
import tensorflow as tf
# 定义占位符(输入节点)
a = tf.placeholder(tf.float32, name='a')
b = tf.placeholder(tf.float32, name='b')
# 定义运算节点
c = tf.add(a, b, name='c')
# 创建会话并执行
with tf.Session() as sess:
result = sess.run(c, feed_dict={a: 3.0, b: 4.0})
print(result) # 输出 7.0
看到没?a和b只是占位符,真正传数据是在sess.run()的时候。这种模式的好处是:图一旦建好,就可以反复执行,而且TF编译器可以对整个图做优化。
我曾经踩过一个坑:在循环里反复创建新的计算图,导致内存爆炸。因为每次创建图,TF都会保留一份图定义,不会自动释放。后来我学乖了,用tf.Graph()显式管理图的生命周期。
到了TensorFlow 2.x,默认启用了Eager Execution(动态图模式),但底层仍然可以用@tf.function装饰器将Python函数转换成静态图。这算是两种哲学的融合吧。
2.4 PyTorch的计算图表示
PyTorch的计算图是动态的,而且是“自动构建”的。你不需要显式定义图,只要执行前向传播,图就自动生成了。来看代码:
import torch
# 定义张量,requires_grad=True表示需要计算梯度
a = torch.tensor(3.0, requires_grad=True)
b = torch.tensor(4.0, requires_grad=True)
# 前向传播,自动构建计算图
c = a + b
# 反向传播,计算梯度
c.backward()
print(a.grad) # 输出 tensor(1.)
print(b.grad) # 输出 tensor(1.)
这里有个关键点:c = a + b这行代码执行时,PyTorch在背后默默构建了一个计算图。这个图记录了c是怎么从a和b计算出来的。当你调用c.backward()时,PyTorch沿着这个图反向传播,计算每个叶子节点的梯度。
注意:PyTorch的动态图每次前向传播都会重新构建。这意味着你可以根据输入数据动态改变网络结构——比如在循环中根据条件决定是否跳过某些层。这在静态图里实现起来非常麻烦。
但动态图也有代价。每次重新建图,编译器就没办法做跨迭代的全局优化。比如算子融合,在静态图里可以轻松做到,在动态图里就得靠JIT编译器(如TorchScript)来补救。
2.5 计算图与编译器优化的关系
好了,现在我们把计算图和编译器联系起来。你想想看,编译器拿到计算图后,能做什么?
- 算子融合:把连续的几个小算子合并成一个大算子,减少内存读写。比如Conv+BN+ReLU可以融合成一个算子。
- 常量折叠:如果某个节点的输入全是常量,编译器可以直接算出结果,省去运行时计算。
- 内存规划:分析张量的生命周期,复用内存空间,降低显存占用。
- 并行调度:找出图中没有依赖关系的节点,放到不同计算单元上并行执行。
这些优化,说白了都依赖计算图提供的信息。没有图,编译器就是无米之炊。
一句话总结:计算图是AI编译器的“中间表示”(IR),所有优化都围绕它展开。理解计算图,是理解AI编译器优化的第一步。
2.6 本章知识体系
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的核心内容串起来了。从计算图的基本概念出发,延伸到静态图与动态图的对比,再到两大主流框架的具体实现,最后落到编译器优化这个实际应用上。你可以在脑子里把这张图过一遍,看看每个知识点之间是怎么关联的。
一个小建议:如果你刚开始接触计算图,别急着看复杂的模型。先拿一个简单的加法或矩阵乘法,手动画出它的计算图,然后用TensorFlow和PyTorch分别实现一遍。亲手做一遍,比看十遍理论都管用。
好了,这一章就到这里。计算图是AI编译器的基石,后面的算子融合、内存优化都建立在这个基础上。把这些概念吃透,后面学起来会轻松很多。