想象两条街上的两家餐厅。一家叫“流水斋”,后厨像中央厨房:顾客还没落座,所有食材已按标准菜谱完成切配,调料汁提前调好,灶台火候按时间轴整齐推进——上菜极快,晚高峰也能稳稳出餐;可你若临时说“不要香菜”,整条流水线都要重新规划。另一家叫“随心小炒”,没有固定菜谱,顾客点什么,大厨就现洗现切现炒,还能边炒边尝——改口味、加配菜都毫不费力,但一到饭点就要排队,每道菜都从零开始。 AI框架里的“计算图”,就是模型计算的“后厨流程图”。静态图像流水斋,先把完整的执行计划建好再开火;动态图像随心小炒,边写算子边立刻出结果。这篇文章用这个比喻拆解两种执行模式的原理、优劣与融合趋势,让你明白为何 PyTorch 调试方便、TensorFlow 部署高效,以及为什么两家餐厅正在互相学习。
学懂静态图与动态图,就摸到了 AI 框架执行模式的分水岭:它直接决定你的调参体验、训练吞吐和部署延迟,是每一位 AI 从业者理解框架本质的必修课。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
计算图概念:模型的工序卡
把一道“番茄炒蛋”端上桌,后厨需要一张工序卡:番茄要洗、切、炒,鸡蛋要打散、下锅,每一步之间都有严格的先后顺序。AI模型的计算同样如此——卷积、矩阵乘法、激活函数之间,存在清晰的数据依赖。张量从一层流到另一层,谁在等谁的结果,必须一笔一笔写清楚。这张描述“谁等着谁”的工序图,就是计算图(Computation Graph)。
计算图用节点表示一次运算(也叫算子),用有向边表示数据的依赖与流转。以最简单的式子 \(y=(a+b)\times c\) 为例:先做加法,再做乘法。把这两个动作画成图,节点和边就构成了模型计算的骨架。深度神经网络本质就是一张巨大的计算图,数亿个参数在其中传递,而框架的调度器拿着这张图决定“哪个算子先跑、哪个算子的结果要被谁消费”。
在AI框架中,计算图 \(G=(V,E)\) 是调度和优化的基础。V 是节点集合,E 是边集合。图建得越完整,编译器越能站在全局视角做优化;图建得越零散,调度器越难施展拳脚。因此,计算图不仅是数据结构,更是框架“运筹帷幄”的沙盘。
回到后厨:这张工序卡本身不决定上菜快慢,真正决定快慢的,是后厨按什么方式使用它——是先完整排好流水线(静态图),还是来一单做一单(动态图)。这正是接下来的两节要讲的两种后厨哲学。
- 节点集合 V列出图中所有参与者,输入张量 a、b、c 是入口食材,+ 和 × 是加工动作,y 是最终出锅的菜。
- 有向边集合 E每条边 \((u,v)\) 表示“u 的结果要流入 v”,例如 \((a,+)\) 表示 a 等待参加加法运算。
- 整张图 G把节点和边打包成一个整体,供调度器按依赖顺序排兵布阵。
静态图原理:中央厨房的流水线
流水斋的后厨从来不在饭点时才开始思考。经理会在前一天晚上把整场宴席的工序写成标准作业流程:食材统一切配,料汁提前调制,灶台按时间轴依次点火。AI框架中的静态图,就是这个思路——它把“构建计算图”和“执行计算”分成两个阶段,先把整张图完整建好,再交给执行引擎统一开火。
这种模式叫“先建后跑”。在构建阶段,你写下的每一行网络代码并不会真正计算数值,而是被框架翻译成图结构里的节点和边,这个过程是惰性求值(Lazy Evaluation):表达式只是被“记下来”,并不立刻算出结果。只有当整张图构建完毕、调用执行接口时,张量才开始在图中流动。
静态图的杀手锏在于:图是完整的,编译器就拥有了上帝视角。它可以做算子融合,把 \(a+b\) 和结果乘 \(c\) 合并成一个内核;可以做内存复用,提前算出每个中间张量的生命周期,让它们共用一块缓冲;还可以做常量折叠,把能提前算出的分支在编译期就干掉。这些优化,靠“边写边算”的零散执行是做不到的。
中央厨房最适合承办大型宴席,却最怕客人临时改菜单。静态图也一样:一旦模型里有复杂的 Python 条件分支、动态形状,或者你想在中间某个节点打个断点看看数值,事情就变得棘手。这是静态图高效的代价,也是动态图登场的理由。
- 构建阶段 Build用户定义的模型函数 \(f\) 被编译成计算图 \(G\),此刻没有数值计算发生。
- 执行阶段 Eval图建立完成后,一次性喂入输入 \(\{a,b,c\}\),框架按依赖顺序执行全部算子。
- 全局优化两个阶段分离后,编译器可以对 \(G\) 先做优化再执行。
动态图原理:现点现炒的小灶台
随心小炒的厨房没有任何预案。客人喊一声“番茄炒蛋,多放糖”,大厨立刻从筐里抓起番茄,打蛋、起锅、翻炒,每一步都当场完成,还能随时尝一口调整口味。AI框架的动态图就是这样:代码执行到哪一行,就把对应的算子立刻算出来,不需要预先构图,也不需要等待执行引擎的统一调度。这种模式叫“边建边跑”,学名是即时求值(Eager Execution)。
在动态图模式下,Python 代码就是计算过程本身。你想打印中间变量?直接 print 就好;你想加个断点?IDE 会在执行到那一行时停住。这让调试体验极其自然,也让研究者可以像写普通程序一样写神经网络——改结构、改分支、看中间结果,全部即时反馈。PyTorch 正是凭借这种极致的易用性,赢得了研究者的青睐。
但自由是有代价的。动态图每到一步都要启动一次算子内核,无法跨越多个算子做全局性的融合与内存规划。而且每次前向传播都可能重新构建图结构,重复调度开销不小。就好比小炒店每道菜都从洗菜开始,无法像中央厨房那样批量备料、共享半成品。
现点现炒的厨房,口味灵活、改菜方便,但高峰时段出餐慢;动态图让你写模型如行云流水,但在大规模训练和部署时,逐算子的调度开销会实打实地反映在速度上。静态图的性能优势和动态图的灵活体验,成了 AI 框架必须权衡的两端。
- 加法立即执行执行到 \(a+b\) 时,数值马上算出并赋给中间变量 \(s\)。
- 乘法继续求值拿到 \(s\) 后,下一步立即与 \(c\) 相乘。
- 全程即时整个过程等价于一次普通函数求值,建图与执行同时发生。
优劣对比:流水线与小灶台的取舍
把流水斋和随心小炒放在同一条街上,就能看清两种模式的利弊。流水斋的优势是稳定与高效:中央厨房可以提前规划所有工序,批量备料、共享灶台、统一出餐。随心小炒的优势是灵活与即时:菜单随时可改,口味随时可调,客人满意是最高优先级。静态图和动态图的取舍,惊人地相似。
性能差异可以用一个简化模型理解:一次前向传播的总时间,约等于所有算子内核的启动开销加上实际计算耗时。静态图通过算子融合把多个小内核合并成一个大内核,减少启动次数;动态图则每个算子独立启动,内核数量多,启动开销自然更高。此外,静态图还能提前规划内存布局,减少碎片和重复分配,这部分收益在长序列、大模型场景中格外明显。
不过,动态图并非毫无优势。它写起来像普通 Python 程序,逻辑直白,分支、循环、调试都自然;静态图则要求模型可静态化,遇到动态形状或复杂控制流往往要改写代码,开发体验大打折扣。所以选择哪种模式,本质上是在问:你更在意出餐速度,还是更在意改菜单的自由度?
宴席选流水斋,下午茶选随心小炒,道理就这么简单。实际工程中,研究阶段追求快速试错,更适合动态图;生产环境追求吞吐和延迟,更适合静态图。但两条街的餐厅不会永远井水不犯河水——接下来看看主流框架是怎么选择的。
- 内核数量计算图中需要启动的算子内核个数,融合前是多个,融合后变成一个。
- 启动开销每个算子在硬件上启动一次都有固定开销,与数据量无关。
- 融合降低 T把相邻算子合并后,内核数从 \(n_{\text{kernel}}\) 降为 \(n'_{\text{kernel}}\),总时间随之下降。
框架落地:两大餐厅的经营风格
不同 AI 框架就像不同风格的餐厅。TensorFlow 早期是典型的“中央厨房派”:1.x 时代使用 Graph + Session,用户必须先定义好完整的计算图,再启动会话执行,静态图哲学贯穿始终。PyTorch 则是“现点现炒派”:默认动态图模式,代码即执行,研究者用起来如鱼得水,迅速成为学术界的宠儿。
但这种对立并不绝对。TensorFlow 2 引入了 eager 模式,默认逐算子执行;PyTorch 则推出了 TorchScript 和 torch.compile,试图把写好的动态代码捕获成静态图进行优化。两大框架都在向对方学习,也催生了框架内部的“前后端分离”架构:前端负责把用户友好的 Python 代码翻译成计算图,后端负责高效执行这张图。
这种分离很像餐厅的“前台点单”和“后厨出品”分工。前台服务员只负责记录客人的需求,后厨则按标准配方统一加工。前端决定好不好写,后端决定跑得快不快。同一个计算图,可以交给 CPU、GPU 或 NPU 执行,硬件差异被后端引擎屏蔽。用户感受到的“易用性”来自前端,而“性能”主要来自后端。
框架的经营风格可以不同,但谁也挡不住“既要好吃又要快”的用户需求。于是,两大流派开始同一条路:把动态图的开发体验和静态图的执行性能结合起来。这正是下一节的“动静合一”。
- 前端翻译用户用 Python 写的网络定义,被框架前端解析为计算图。
- 后端执行图交给后端执行引擎,映射到具体硬件内核。
- 硬件解耦同一张图在不同硬件上只需更换后端实现,无需重写模型。
融合演进:动静合一的新后厨
你有没有想过:为什么现代餐厅不能既让顾客自由点单,又让后厨拥有中央厨房的效率?答案是——把“点单”和“备餐”分开。顾客在前台自由选择(动态),后厨按标准化配方批量生产(静态)。AI 框架的融合演进,正是这条路径:用动态图写模型,用静态图优化执行。
实现这一愿景的关键技术是图捕获(Graph Capture)。框架先让动态代码“试跑”一次,用假的输入张量走一遍前向计算,把这次运行的算子调用顺序记录成一张静态计算图。PyTorch 的 torch.compile、TensorFlow 的 tf.function,本质都是这个套路:先把动态函数变成图,再对图做全局优化。
图捕获之后,框架还会进行即时编译(JIT Compile):对捕获到的图做算子融合、内存规划、循环优化,最终生成一个高效的编译后函数。此后每次调用,都直接执行这份编译产物,不再重复逐算子调度。用户依然写的是动态代码,享受的是静态图的性能,这就是“动静合一”。ONNX 导出和 TensorRT 部署也是同样思路:把训练好的动态模型固化为静态推理图。
这场演进让“流水斋”和“随心小炒”不再是竞争对手,而是同一个餐厅的两个部门。理解了这一点,你就能看懂为什么 PyTorch 2.0 的 torch.compile 让训练提速、为什么 TensorFlow 的 tf.function 能让 Python 代码跑出图执行的速度。那在实际工作中,到底该如何选型呢?
- 轨迹捕获 Trace用假输入 \(x_{\text{dummy}}\) 运行动态函数 \(f\),把执行的算子序列记录成图。
- 图优化 Optimize对捕获的图做算子融合、内存复用等全局优化。
- 编译执行 Compile把优化后的图编译成可直接调用的函数,后续输入直接走编译产物。
选型思路:你的餐厅该学谁
如果你是研究院的“实验餐厅”,菜单天天变,新菜品层出不穷,点单的客人(或者说论文的 deadline)随时可能提出新需求——动态图是你的最佳选择。快速迭代、随时打印中间张量、像写普通程序一样改模型,这些能力在研究阶段千金不换。PyTorch 在学术界统治性的地位,正源于这种极致的开发体验。
如果你是产品团队的“连锁餐厅”,要服务千万级用户,对延迟和吞吐有硬指标——静态图或编译优化后的执行模式更合适。将训练好的模型导出为 ONNX、TensorRT 或使用 torch.compile 编译,可以让推理速度获得数倍提升。生产环境不在乎开发时多费点功夫,只在乎最终端上桌的速度和稳定度。
现实中最优解常是混合策略:研究阶段用动态图快速验证,部署阶段将模型固化为静态图或编译图。甚至在同一张模型图中,也可以划分静态子图和动态子图:能静态化的大块计算(卷积、矩阵乘)交给编译器优化,必须动态处理的控制流单独保留。这样既保住灵活性,又拿到性能。
回到开头的比喻:世界上不需要两家一模一样的餐厅。研究阶段的食堂要灵活,生产阶段的连锁店要效率,而聪明的餐饮集团,学会了把两种后厨装进一家店。静态图与动态图不是二选一的敌人,而是可以按需组合的两种后厨哲学。
- 静态子图模型中可以提前编译、形状固定的部分,如连续堆叠的卷积与全连接。
- 动态子图依赖 Python 控制流、动态形状的部分,保留即时执行。
- 整体组合两部分通过张量接口拼接,用户感知仍是同一个模型。
AI 应用场景
科研实验与模型设计
阶段:模型研究与快速原型
解决的问题:研究者频繁改动网络结构、调试训练过程。如果使用纯静态图,每次改动都要重新构图、重编译,中间张量无法直观打印,迭代效率极低。动态图让代码即执行,研究者可以像调试普通 Python 程序一样调试深度网络,随时打印 loss、梯度或任意中间层的输出。
具体案例:某高校实验室开发一个新的注意力机制,用 PyTorch 动态图实现后,在每一层后打印张量形状和数值分布,快速定位激活值爆炸的位置,当天就完成了原型验证。同样的工作如果用纯静态图,至少需要多花数天进行图调试。
生产部署与推理优化
阶段:模型部署与推理优化
解决的问题:动态图在训练时灵活高效,但直接部署到线上服务时,逐算子启动的开销会放大延迟。将训练好的模型通过 ONNX 导出或 TensorRT 转换为静态图,可以在部署前完成算子融合、精度校准和内存复用,显著降低推理延迟、提升吞吐。
具体案例:某电商推荐系统将 PyTorch 模型导出为 ONNX,再经 TensorRT 转换为静态化推理引擎,在 GPU 上推理延迟从 8ms 降到 2ms,QPS 提升 3 倍。整个过程无需重写模型代码,只改变部署阶段的执行模式。
大规模训练加速
阶段:训练性能优化
解决的问题:大模型训练时,纯动态图的逐算子调度成为吞吐瓶颈。利用 torch.compile 等动静融合技术,可以在保持动态开发体验的同时,把热点区域捕获为静态图进行编译优化,让训练吞吐显著提升。
具体案例:某大模型团队使用 torch.compile 编译 Transformer 训练脚本,在相同 GPU 资源下训练吞吐提升 40%。模型代码几乎无需改动,只增加一行 compile 调用,就获得了接近静态图的执行效率。
Python 演示
import operator class TinyGraph: """极简静态计算图:先构图,run 时统一执行。""" def __init__(self): self.slots = {} self.plan = [] def input(self, name, value=None): self.slots[name] = value return name def op(self, name, fn, *deps): self.plan.append((name, fn, deps)) return name def run(self, **feed): slots = dict(self.slots) slots.update(feed) for name, fn, deps in self.plan: args = [slots[d] for d in deps] slots[name] = fn(*args) return slots def eager_calc(a, b, c): """动态图风格:执行到哪行就算到哪行。""" s = a + b print("[动态] 中间结果 s =", s) y = s * c return y def static_calc(a, b, c): """静态图风格:先把计划写全,再一次执行。""" g = TinyGraph() g.input("a") g.input("b") g.input("c") s = g.op("s", operator.add, "a", "b") y = g.op("y", operator.mul, "s", "c") print("[静态] 执行前计划:", g.plan) result = g.run(a=a, b=b, c=c) return result["y"] if __name__ == "__main__": a, b, c = 2.0, 3.0, 4.0 print("动态计算结果:", eager_calc(a, b, c)) print("静态计算结果:", static_calc(a, b, c))
self.plan.append((name, fn, deps))把一次运算记入“后厨计划表”,只登记不开火。slots[name] = fn(*args)真正执行:从依赖槽位取值,喂给算子,存入结果。s = a + b动态图风格:这一行执行时就立刻算出数值,还能打印。print("[静态] 执行前计划:", g.plan)静态图在开跑前能看到完整工序表,这是它可全局优化的根基。result = g.run(a=a, b=b, c=c)一次性喂入所有输入,按计划顺序完成全部计算。
常见误区
误区“PyTorch 底层只能动态执行,TensorFlow 只能先建图再跑,两者水火不容。我学 PyTorch 就不用关心静态图了。”
正解两大框架早已互相借鉴。TensorFlow 2 默认开启 eager 执行(动态图),同时通过 tf.function 把 Python 函数编译为静态图;PyTorch 则提供了 TorchScript 和 torch.compile,能把动态代码捕获为静态图进行优化。现代框架几乎都是“一条腿动态、一条腿静态”,只掌握一种模式已经不够了。
为何常见:很多人接触框架时只用了默认模式——PyTorch 默认 eager、TensorFlow 1.x 默认 Graph,自然产生了“某框架只属于某流派”的刻板印象。
误区“静态图有全局优化,所以无论什么模型、什么硬件,静态图执行速度一定碾压动态图。”
正解静态图的优势来自全局优化空间,但实际性能还取决于算子实现质量、数据布局、硬件调度等因素。若模型形状极端动态、控制流复杂,静态图可能编译不充分,甚至退化为低效执行。动态图配合优化后的算子库(如 cuDNN、OneDNN),在不少场景下也能逼近静态图性能。正确理解是:静态图拥有更大的优化空间,但“一定更快”是过度简化。
为何常见:人们容易把“理论上限”当作“实际结果”,忽略了编译器优化成本、动态形状等现实约束。
误区“线上推理必须用静态图,动态图模型根本没法部署,产品化只能用 TensorFlow/TensorRT。”
正解动态图模型可以通过多种方式部署:torch.onnx.export 导出为 ONNX 静态图,TorchScript 序列化为可优化图,TensorRT 转换后静态化执行。事实上,PyTorch 模型在工业界大规模落地,靠的就是这种“动态研发、静态部署”的流水线。动态图是开发范式,不是部署障碍。
为何常见:初学者看到动态图逐算子调度开销大,便误以为它“不能用于生产”,忽略了框架提供的各类导出和编译工具。
误区“静态图把整张图编成一个黑盒,中间结果全看不到,出 bug 只能靠猜。”
正解静态图调试确实不如动态图直观,但并非完全不可调。TensorFlow 的 tf.print 会在图中插入打印节点,TensorBoard 可以可视化图结构和张量分布;还可以通过子图执行、符号断点定位问题。而且现代框架常用“先动态跑通、再编译优化”的流程,把调试留在了动态阶段。静态图调试难是事实,但“完全无法调试”是夸张。
为何常见:静态图的构建与执行分离,让习惯了 Python 逐行调试的人感到陌生,于是夸大了调试难度。
面试题
Q1. 静态图最典型的执行特点是?
- A.边定义边计算
- B.先建图再执行← 正确答案
- C.每次调用重编译
- D.只支持单机执行
答案:B。静态图的核心是“先建后跑”:先构建完整计算图,再统一执行。边定义边计算是动态图的特点;每次调用重编译与静态图意图相悖;静态图同样支持多机多卡,D 是错误干扰项。
Q2. 动态图调试方便的主要原因是什么?
- A.图结构能够提前打印
- B.代码执行过程即计算过程← 正确答案
- C.自带自动可视化工具
- D.算子数量本身较少
答案:B。动态图采用即时求值,Python 代码执行到哪一行,算子就立刻计算到哪一步,因此可以像调试普通程序一样打印中间值、下断点。A 是静态图的特征;C 不是动态图独有的;D 与调试便利性无关。
Q3. “惰性求值”在静态图中的含义是?
- A.表达式的计算被推迟← 正确答案
- B.表达式的计算被加速
- C.表达式被重复执行
- D.表达式被立即求值
答案:A。惰性求值指表达式在构建图时只是被记录,真正的数值计算被推迟到整张图构建完成后统一执行。B 把延迟计算误认为加速;D 是动态图的即时求值;C 描述的是重复执行,与惰性求值无关。
Q4. 静态图的编译优化通常包括哪些手段?
- A.合并相邻算子← 正确答案
- B.动态展开所有分支
- C.复用中间内存← 正确答案
- D.即时解释执行代码
答案:AC。算子融合(合并相邻算子)减少内核启动次数,内存复用通过生命周期分析共享缓冲,二者都是静态图典型优化。动态展开分支不是通用手段,即时解释执行是动态图的特征而非静态图优化。
Q5. torch.jit.trace 捕获计算图的机制是?
- A.静态分析源码生成图
- B.运行一次并记录算子序列← 正确答案
- C.根据形状自动推导图
- D.手工编写算子执行清单
答案:B。torch.jit.trace 使用假输入实际运行一次模型代码,把这次执行中调用的算子按顺序记录下来,形成计算图。它不是静态分析源码,也不是形状推导,更不需要手工编写清单。这也说明了为什么 trace 对数据相关控制流无能为力。
Q6. 关于算子融合,下列说法正确的是?
- A.减少内核启动次数← 正确答案
- B.增加中间张量写回
- C.降低数据访存开销← 正确答案
- D.保证模型精度提升
答案:AC。算子融合将多个算子的计算合并到一个内核中,避免中间张量写入显存再读回,既减少了内核启动次数,又降低了访存开销。B 说反了;融合不改变数学结果,与精度提升无必然关系。
Q7. 动态图的主要优势是哪一项?
- A.中间结果容易打印← 正确答案
- B.图结构先完整生成
- C.适合生产环境部署
- D.部署无需任何优化
答案:A。动态图边建边跑,执行到任意一行都可以打印中间张量,这是它调试方便的根本原因。B 是静态图特征;C、D 不符合实际——动态图部署通常需要导出或编译优化。
Q8. tf.function 对包含 Python 条件分支的函数如何处理?
- A.直接报错拒绝编译
- B.用 AutoGraph 转换为图内控制流← 正确答案
- C.原样保留 Python 运算
- D.无条件展开全部分支
答案:B。tf.function 会启用 AutoGraph 机制,把 Python 的 if/for 等控制流自动转换为 TensorFlow 图内的条件与循环算子,从而完整地固化为静态图。A 过于绝对;C 会导致图不完整;D 对未知次数的循环不现实。
Q9. 静态图惰性求值的典型表现是?
- A.每次运算立即返回数值
- B.运算被记录到图中稍后执行← 正确答案
- C.使用 Python 解释器逐步执行
- D.依赖运行时动态添加节点
答案:B。惰性求值意味着构建图阶段只记录算子调用,真正的数值计算被推迟到图执行阶段。A 是即时求值;C 是动态图的行为;D 描述的是动态图执行时不断建图的现象,与静态图“先建后跑”相反。
Q10. 动态图在工程中的典型应用场景是?
- A.中间结果容易打印← 正确答案
- B.图结构先完整生成
- C.适合快速原型迭代← 正确答案
- D.部署无需任何优化
答案:AC。动态图的即时求值让中间结果随手可查,非常适合研究阶段的快速原型迭代。B 是静态图的特征;D 不符合实际,动态图部署通常仍需导出为静态图或编译图进行优化。