要搞懂计算图的底层原理,核心是拆解它的 "三步走流程":构建图 → 优化图 → 执行图 。我们就用你之前的核心例子(a_regular_function 函数),一步步扒开底层到底做了什么------全程不脱离代码,每个底层操作都对应到具体函数逻辑。
先重申例子的核心运算(方便对照):
python
def a_regular_function(x, y, b):
x = tf.matmul(x, y) # 运算1:矩阵乘法(x是2D张量,y是2D张量)
x = x + b # 运算2:加法(矩阵+标量,广播机制)
return x
# 输入张量:x1(1×2)、y1(2×1)、b1(标量)
x1 = tf.constant([[1.0, 2.0]])
y1 = tf.constant([[2.0], [3.0]])
b1 = tf.constant(4.0)
一、先明确计算图的"底层组成单元"
计算图的本质是 "无状态的运算依赖图",底层由3个核心部分构成,对应例子拆解:
| 组成单元 | 通俗理解 | 例子中的具体对应 |
|---|---|---|
| 节点(Node) | 一个"运算指令"(比如乘法、加法),是图的"操作单元" | 2个节点:tf.matmul 节点、+ 节点 |
| 边(Edge) | 节点间的"数据依赖",传递的是张量(Tensor) | 3条边: 1. x1 → tf.matmul(输入边) 2. y1 → tf.matmul(输入边) 3. tf.matmul的输出 → +(中间边) 4. b1 → +(输入边) 5. +的输出 → 函数返回(输出边) |
| 张量(Tensor) | 边传递的数据(不可变),是图的"数据单元" | 输入张量(x1、y1、b1)、中间张量(tf.matmul的结果)、输出张量(最终x) |
简单说:计算图就是把"运算"和"数据依赖"用"节点-边"的结构固定下来,形成一份"不可修改的运算说明书"。
二、计算图的底层三步骤(对应例子拆解)
当你调用 tf.function 包装后的函数时,底层会按以下三步执行(这就是"图执行"的核心):
第一步:构建图(Graph Construction)------ 扫描函数,画"运算说明书"
这一步的核心是:tf.function 不执行函数里的运算,只扫描其中的 TensorFlow 操作,记录"谁依赖谁",最终画出计算图。
对应例子的构建过程:
- tf.function 会"读"
a_regular_function的代码,发现两个 TensorFlow 操作:tf.matmul和+; - 分析依赖关系(谁必须在谁之前执行):
- 要算
x + b,必须先拿到tf.matmul(x, y)的结果(因为x被tf.matmul更新了); - 所以依赖顺序是:
x1、y1 → tf.matmul → 中间结果 → +(b1) → 最终输出;
- 要算
- 按照这个依赖,创建计算图的节点和边:
- 节点1:
tf.matmul,标记输入为"x的占位、y的占位",输出为"中间矩阵"; - 节点2:
+,标记输入为"中间矩阵、b的占位",输出为"最终矩阵"; - 边:连接
tf.matmul的输出 →+的输入,形成完整流程;
- 节点1:
- 注意:这一步完全不做任何数值计算 !比如
tf.matmul(x1, y1)不会真的算出[[1*2 + 2*3]] = [[8.0]],只是记录"要做这个乘法",以及它的输入输出是什么。
关键细节:第一次调用
a_function_that_uses_a_graph(x1, y1, b1)时,会触发这个"构建图"的过程(所以第一次调用稍慢,叫"热身");之后再调用时,会直接复用已构建好的图,不再重新扫描函数。
第二步:优化图(Graph Optimization)------ 给"运算说明书"瘦身提速
这是计算图的核心优势之一!TensorFlow 有一个专门的"优化器"(Graph Optimizer),会对第一步构建的原始图做"等价变形",目的是 减少运算次数、降低内存开销、适配硬件(CPU/GPU/TPU)。
对应例子的优化过程(底层会做这些操作):
- 常量折叠(Constant Folding) :
- 例子中
y1([[2.0], [3.0]])和b1(4.0)是常量张量,优化器会提前计算"常量相关的固定逻辑"------比如知道y1是固定的2×1矩阵,会在图中直接记录它的形状,避免执行时再解析; - 极端情况:如果
x也是常量,优化器会直接把tf.matmul(x, y) + b的结果算出来,图中只剩下一个"输出常量"的节点(彻底省去运算)。
- 例子中
- 运算融合(Operator Fusing) :
- 原始图是"matmul → +"两个独立节点,优化器会把它们融合成一个"matmul + add"的复合节点;
- 好处:减少节点间的张量传递开销(不用先把 matmul 的结果存到内存,再读出来做加法),GPU 执行时能一次性完成,速度更快。
- 形状推断(Shape Inference) :
- 优化器会提前推断所有张量的形状:
x1(1×2) × y1(2×1) → 中间结果(1×1),再加上b1(标量,广播成1×1),最终输出(1×1); - 好处:执行时不用再检查张量形状是否匹配,避免运行时错误,同时让硬件提前分配内存。
- 优化器会提前推断所有张量的形状:
优化后的图,本质还是"做 matmul + add",但流程更精简、执行效率更高------这就是"图执行"比"即时执行"快的核心原因。
第三步:执行图(Graph Execution)------ 按优化后的"说明书"算结果
这一步才是真正做数值计算的阶段,底层流程是:
- 把输入张量(x1、y1、b1)"喂"给优化后的图的"输入占位"(对应第一步构建图时的"x占位、y占位、b占位");
- 硬件(CPU/GPU)按图的节点顺序(优化后的顺序,比如融合后的"matmul+add"节点)执行运算:
- 先算
x1 × y1 = [[1*2 + 2*3]] = [[8.0]](matmul 部分); - 再算
8.0 + 4.0 = 12.0(add 部分);
- 先算
- 把最终结果(
[[12.0]])从图的"输出边"返回。
关键对比:即时执行(普通函数)是"逐行解析→执行→解析→执行",每次调用都要重复解析和执行;而图执行是"一次构建+优化,多次复用执行" ------后续再调用 a_function_that_uses_a_graph(x2, y2, b2) 时,直接跳过第一步和第二步,只用把新的 x2、y2、b2 喂给已优化的图,直接执行第三步,速度极快。
三、用"对比表"看清底层差异(普通函数 vs tf.function)
为了让你更直观理解"底层做了什么",我们对比普通函数(即时执行)和 tf.function(图执行)的底层流程:
| 执行步骤 | 普通函数(a_regular_function) | tf.function 包装后的函数(图执行) |
|---|---|---|
| 每次调用时 | 1. 解析 tf.matmul(x, y) → 执行 → 得到结果; 2. 解析 x + b → 执行 → 得到结果; 3. 返回结果。 |
1. 第一次调用: - 扫描函数 → 构建图; - 优化图; - 执行图 → 返回结果; 2. 后续调用: - 直接复用优化后的图 → 执行 → 返回结果。 |
| 底层核心 | 无图,"边解析边执行",无优化 | 有图,"一次构建优化,多次执行",有多层优化 |
| 速度关键 | 每次都要解析操作,速度慢(尤其调用次数多) | 复用图,跳过解析和优化,速度快(模型越大、调用越多,优势越明显) |
四、总结:计算图的底层原理一句话概括
用 tf.function 包装函数后,底层会先 "扫描函数里的 TensorFlow 操作,按依赖关系构建原始计算图" ,再通过优化器 "给图瘦身提速(常量折叠、运算融合等)" ,最后 "用输入张量喂给优化后的图,执行运算并返回结果"------后续调用直接复用优化图,省去重复构建和解析的开销,这就是计算图的核心原理。