2、计算图基础:计算图的概念、静态图与动态图、计算图与算子融合的关系

2.1 计算图到底是什么?

说实话,我刚入行那会儿,觉得“计算图”这词挺唬人的。后来自己手写了一个简单的神经网络,才明白它就是个“流水线图纸”。

计算图,说白了就是用图结构来描述数学运算。每个节点是一个操作(比如矩阵乘法、ReLU激活),每条边是数据流动的方向(也就是张量)。你想想看,一个神经网络前向传播的过程,其实就是数据沿着这张图一路跑下去。

核心要点:计算图 = 节点(算子)+ 边(数据依赖)。它不存具体数值,只存“谁依赖谁”的关系。

举个例子,一个简单的表达式 z = (a + b) × c,它的计算图长这样:

a b c + × z

你看,数据从 a、b 流入加法节点,结果再和 c 一起流入乘法节点,最后输出 z。这就是一张最简单的计算图。

2.2 静态图 vs 动态图:两种哲学

这里有个经典问题:静态图和动态图到底选哪个?我记得刚接触 PyTorch 时,觉得动态图太爽了,写起来跟普通 Python 代码一样。后来去优化一个推理服务,才发现静态图在性能上确实有它的优势。

对比维度 静态图(如 TensorFlow 1.x) 动态图(如 PyTorch)
构建方式 先定义图,再执行 边定义边执行
调试难度 较难,不能直接打断点 容易,跟普通代码一样
性能优化 容易做全局优化 优化空间有限
灵活性 低,控制流受限 高,支持任意 Python 逻辑
典型框架 TensorFlow 1.x, Caffe PyTorch, TensorFlow Eager

我的经验:做研究、快速原型用动态图;做部署、追求极致性能用静态图。现在很多框架已经能两者兼顾了,比如 PyTorch 的 torch.jit.trace 可以把动态图转成静态图。

为什么会这样?静态图在运行前就能看到整个计算流程,可以做算子融合、内存复用等优化。动态图虽然灵活,但每次执行都要重新构建图,优化器没法提前做全局规划。

2.3 计算图与算子融合的关系

好,重点来了。算子融合和计算图到底什么关系?

我个人习惯把计算图想象成一张“菜谱”。每个步骤(算子)都写得很清楚:先切菜,再炒菜,最后装盘。算子融合就是把这些步骤合并——比如把“切菜”和“炒菜”合并成一个“切炒一体”的操作。

在深度学习里,最常见的融合是 Conv + BN + ReLU。你看这张图:

融合前: Conv BN ReLU 融合后: Conv-BN-ReLU 融合算子

融合前,数据要经过三次读写:Conv 写中间结果、BN 读再写、ReLU 读再写。融合后,这三个操作合并成一个 kernel,中间结果直接留在寄存器里,不用来回访存。

注意:不是所有算子都能融合。融合的前提是这些算子在计算图上必须是“顺序依赖”的,中间不能有分支。我曾经踩过一个坑,把两个有数据依赖的算子强行融合,结果精度全乱了。

算子融合的本质,就是利用计算图的拓扑结构,把多个连续的、无分支的节点合并成一个。这样做的好处很明显:

  • 减少访存:中间结果不用写回显存,直接在片上搞定
  • 减少 kernel 启动开销:原来要启动 3 次 GPU kernel,现在 1 次搞定
  • 利于编译器优化:融合后的算子可以做更多局部优化

2.4 实战中的避坑指南

我曾经在一个项目里,为了追求极致性能,把整个 ResNet 的残差块都融合成一个算子。结果呢?训练速度没提升多少,反向传播的梯度计算却变得极其复杂,调试了整整两天。

这里给大家几个建议:

  1. 先分析计算图的热点:用 profiling 工具看看哪些算子耗时最多,优先融合这些
  2. 保持反向传播的可导性:融合后的算子必须能正确计算梯度,否则训练会崩
  3. 注意数值精度:融合操作可能会改变中间结果的精度,尤其是混合精度训练时
  4. 不要过度融合:融合太狠会导致显存碎片化,反而降低性能

一句话总结:计算图是算子融合的地图。你只有把地图看懂了,才知道哪些路可以合并,哪些路必须分开走。

嗯,关于计算图的基础就聊这么多。下一节我们会深入具体的融合策略,包括横向融合、纵向融合,以及它们在 Transformer 模型中的实际应用。

可可精品资料库