计算图优化基础:计算图的概念、静态图与动态图、图优化策略
各位同学,今天我们来聊聊计算图优化。这玩意儿,说白了就是模型部署的「地基」。地基没打好,后面算子优化得再花哨,性能也上不去。我个人习惯,拿到一个模型,第一件事不是看参数,而是先把它的计算图画出来,看看有没有「脏活累活」可以省掉。
一、计算图的概念
计算图,你可以把它想象成一张「菜谱」。菜谱上写着:先切菜,再热油,然后下锅翻炒。计算图也一样,它把神经网络里的所有运算,比如卷积、ReLU、矩阵乘法,都拆成一个一个的节点(Node),节点之间的连线就是数据流动的方向(Tensor)。
举个例子,一个简单的表达式 z = (x + y) * w,它的计算图长这样:
输入: x, y, w
节点1: add(x, y) -> temp
节点2: mul(temp, w) -> z
输出: z
每个节点就是一个算子,每条边就是中间结果。我在项目中遇到过,有些新手工程师喜欢把计算图画得特别复杂,一个节点里塞了七八个操作。嗯,这里要注意,计算图越「细粒度」,优化的空间就越大。你想想看,细到每个加减乘除都能单独优化,那性能提升的空间自然就大了。
核心要点:计算图是模型部署的「中间表示」。它把神经网络从「黑盒」变成了「白盒」,让我们能看清楚每一步在干什么。
二、静态图与动态图
这里有个经典问题:静态图和动态图,到底选哪个?
静态图,就像你提前写好剧本,演员照着演。框架(比如 TensorFlow 1.x、PyTorch 的 TorchScript)先把你写的代码「扫」一遍,构建出一张完整的计算图,然后才真正开始执行。好处是,框架能看到全局,可以做很多「预谋」好的优化。
动态图,就像即兴表演,演到哪算哪。框架(比如 PyTorch 的 Eager Mode)每执行一行代码,就动态地构建一个子图,然后立刻执行。好处是调试方便,写起来像写普通 Python 代码一样自然。
我个人的经验是:训练用动态图,部署用静态图。为什么?
| 特性 | 静态图 | 动态图 |
|---|---|---|
| 构建时机 | 执行前 | 执行时 |
| 调试难度 | 高(报错信息不直观) | 低(像普通代码一样调试) |
| 优化空间 | 大(全局优化) | 小(局部优化) |
| 典型框架 | TensorFlow 1.x, ONNX | PyTorch, TensorFlow 2.x |
我曾经在部署一个 BERT 模型时,用了动态图直接推理,结果发现 GPU 利用率只有 30%。后来换成静态图,把计算图「固化」下来,再做一些算子融合,GPU 利用率直接飙到 85%。这就是静态图的威力——它能看到全局,知道哪些地方可以「合并同类项」。
避坑指南:如果你用 PyTorch,记得用 torch.jit.script 或 torch.jit.trace 把动态图转成静态图。我曾经因为忘了这一步,导致模型在手机上推理慢了 3 倍。
三、图优化策略
有了计算图,我们就可以「动手脚」了。图优化,说白了就是「偷懒」的艺术——把能省的计算省掉,把能合并的操作合并掉。常用的策略有两个:常量折叠和算子融合。
1. 常量折叠
常量折叠,顾名思义,就是把那些「永远不变」的计算提前算好。比如:
# 原始计算图
a = 3.14
b = 2.0
c = a * b # 这个结果永远都是 6.28
d = c + input_tensor
优化后:
# 常量折叠后的计算图
d = 6.28 + input_tensor
你看,a * b 这个乘法节点被直接「折叠」掉了,变成了一个常量 6.28。别小看这个优化,模型里这种「常量 + 变量」的模式特别多。我在项目中遇到过,一个 ResNet-50 模型,光常量折叠就能省掉 5% 的计算量。
注意:常量折叠不是万能的。如果常量计算涉及浮点数精度问题(比如 1.0 / 3.0),折叠后的结果可能会有微小差异。我建议只在推理精度要求不高的场景下使用。
2. 算子融合
算子融合,是图优化里最「香」的一招。它的核心思想是:把多个连续的算子「合并」成一个算子,减少内存读写和 Kernel 启动开销。
举个最常见的例子:Conv + BN + ReLU 融合。
# 原始计算图
input -> Conv2d -> BatchNorm -> ReLU -> output
# 算子融合后的计算图
input -> Conv2d_BN_ReLU -> output
为什么能融合?因为 Conv、BN、ReLU 这三个操作是「串行」的,而且 BN 和 ReLU 的计算量相对较小。把它们合并成一个 Kernel,数据就不用从显存里读出来写进去三次,一次搞定。你想想看,这能省多少带宽?
我个人的经验是:算子融合是性价比最高的优化手段。有时候你花一周时间手写一个算子,性能提升可能只有 10%。但做一个 Conv+BN+ReLU 融合,可能只需要改几行配置,性能就能提升 20%-30%。
常见的算子融合模式:
- Conv + BN + ReLU:最经典,几乎每个 CNN 模型都能用
- Conv + Add + ReLU:ResNet 里的残差结构
- MatMul + Add + ReLU:Transformer 里的全连接层
- LayerNorm + Add:Transformer 里的残差连接
实战建议:如果你用 TensorRT,它自动帮你做算子融合。但如果你自己写推理引擎,我建议先做「水平融合」(同一层内的多个操作合并),再做「垂直融合」(不同层之间的操作合并)。顺序搞反了,效果会打折扣。
四、总结
好了,今天的内容就这些。我们来捋一捋:
- 计算图是模型部署的「蓝图」,把神经网络拆成节点和边
- 静态图适合部署,动态图适合训练,别搞混了
- 常量折叠省掉「无用功」,算子融合减少「搬运工」
下一章,我会带大家手写一个简单的图优化 Pass,把今天讲的理论落地。嗯,到时候你们就知道,纸上谈兵和真刀真枪的区别了。
课后思考:如果你的模型里有 Conv + ReLU + Conv + ReLU 这样的结构,你会怎么融合?是两两融合,还是四个一起融合?为什么?