在大模型推理领域中,图模式(graph mode)中的动态图和静态图是什么意思?

好的,这是一个在大模型推理和深度学习框架中非常核心的概念。我会用尽可能清晰的方式解释动态图和静态图的区别、原理以及它们在大模型推理中的权衡。

核心思想:何时构建计算图?

想象一下,你的模型(比如一个Transformer)是由许多层(如线性层、注意力机制、激活函数等)组成的。这些层之间的数据流动和计算关系可以表示为一个计算图

动态图静态图 最根本的区别就在于这个计算图的构建时机执行方式


1. 动态图

定义边定义,边执行

在动态图模式下,计算图是在代码运行时动态生成的。你每执行一行定义操作的代码(例如 z = x + y),这个操作就立即被执行,并且计算图也会实时地扩展一个节点。

类比:这就像Python的交互式解释器。你输入一行命令,它立刻给你返回结果。

技术特点

  • 直观易懂:代码的执行流程和标准的Python程序完全一致,便于调试。你可以使用 print 语句、断点等所有熟悉的调试工具来检查任何一个中间变量的值。
  • 灵活性高:因为图是动态的,所以你可以轻松使用Python的控制流,如 if-elseforwhile 循环,根据数据的不同动态改变图的结构。

例子(PyTorch的默认模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch

# 动态图示例
x = torch.tensor([1.0], requires_grad=True)
y = torch.tensor([2.0], requires_grad=True)

# 这行代码执行时,加法操作立即完成,图也动态构建了这一步
z = x + y # 图节点:Add

# 执行时,又动态构建了后续的图
a = z * y # 图节点:Mul

print(a) # 输出:tensor([6.], grad_fn=<MulBackward0>)
# 可以随时打印中间结果,例如 z

在大模型推理中的优势与劣势

  • 优势
    • 开发调试便捷:对于模型结构的探索和调试非常友好。
  • 劣势
    • 运行时开销大:每个操作都需要由Python解释器来调度,框架无法看到整个计算的全貌,因此难以进行深度的全局优化。
    • 性能通常较低:对于计算密集型的推理任务,这种频繁的调度和无法优化的问题会导致速度不如静态图。
    • 部署不友好:通常需要依赖Python环境。

2. 静态图

定义先定义,后执行

在静态图模式下,你需要先完整地定义整个计算图的结构,然后再向图中“喂”数据并执行计算。图的定义和执行是分开的两个阶段。

类比:这就像编译C++程序。你先写好所有源代码(定义图),然后用编译器一次性编译成高效的机器码(图优化),最后运行这个可执行文件(执行图)。

技术特点

  • 全局视野:因为框架在执行前就看到了整个计算图,所以可以进行大量的优化,例如:
    • 算子融合:将多个小操作(如:卷积 + 偏置 + ReLU)融合成一个大的核函数,减少内存读写和内核启动开销。
    • 内存优化:预先分配和复用内存,避免动态分配的开销。
    • 常数折叠:将计算图中可以预先计算出的常数节点替换成结果。
  • 高性能:经过优化后,静态图的执行效率通常远高于动态图,尤其适合部署和推理。
  • 调试困难:由于计算图是先定义的,你在定义阶段无法得到具体的计算结果,也无法使用常规的调试工具检查中间节点。调试过程更像是在“审查一个计划”,而不是“观察一个过程的执行”。

例子(TensorFlow 1.x 风格 / PyTorch TorchScript)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# TensorFlow 1.x 风格的静态图(现已不常用,但概念清晰)
import tensorflow as tf

# 1. 定义图阶段
graph = tf.Graph()
with graph.as_default():
x = tf.placeholder(tf.float32, name='x')
y = tf.placeholder(tf.float32, name='y')
z = x + y
a = z * y

# 2. 执行图阶段
with tf.Session(graph=graph) as sess:
result = sess.run(a, feed_dict={x: 1.0, y: 2.0})
print(result) # 输出:6.0
1
2
3
4
5
6
7
8
9
10
11
# PyTorch 的 TorchScript(一种创建静态图的方法)
class MyModel(torch.nn.Module):
def forward(self, x, y):
z = x + y
a = z * y
return a

model = MyModel()
# 通过追踪(tracing)将动态图代码转换为静态图
traced_model = torch.jit.trace(model, (torch.tensor([1.0]), torch.tensor([2.0])))
# traced_model 现在是一个静态图,可以高效执行或序列化保存

在大模型推理中的优势与劣势

  • 优势
    • 极致性能:通过全局优化,推理速度最快,延迟低,吞吐量高。
    • 部署友好:可以将整个计算图序列化为一个独立文件(如PyTorch的 .pt,TensorFlow的 .pb),脱离Python环境运行,便于在移动端、服务器端或专门的推理芯片(如NPU)上部署。
  • 劣势
    • 灵活性差:无法使用原生的Python控制流来构建动态结构的图。虽然现代框架(如PyTorch的TorchScript)通过将控制流编译进图内来提供支持,但其灵活性和可调试性依然不如动态图。
    • 开发体验不友好:调试和构建过程较为繁琐。

总结与趋势

特性 动态图 静态图
构建时机 边定义边执行 先定义,后执行
性能 较低(运行时开销) (全局优化)
灵活性 (支持动态控制流) 低(图结构固定)
调试 容易(标准Python工具) 困难
部署 不友好(依赖Python) 友好(可序列化)
典型代表 PyTorch(Eager Mode) TensorFlow 1.x, PyTorch(TorchScript), TensorFlow Lite

在大模型推理领域的现状与趋势

对于大模型推理,核心诉求是低延迟、高吞吐、低成本,因此静态图是绝对的主流和首选

  • PyTorch:虽然以动态图起家并因此深受研究人员喜爱,但在生产部署时,普遍会使用 TorchScriptFX 等工具将训练好的动态图模型转换(JIT) 成静态图,以获得最佳推理性能。
  • TensorFlow:从2.x版本开始,默认也是动态图(Eager Mode),但通过 @tf.function 装饰器可以轻松地将Python函数转换为静态图,兼顾了易用性和性能。
  • 专门推理框架:如 NVIDIA TensorRTONNX Runtime 等,它们接收的标准输入(如ONNX模型)本身就是一种静态图表示。它们会在此基础上做极其激进的图优化,以在特定硬件上达到极致性能。

结论

在大模型推理领域,“动态图用于训练和原型开发,静态图用于优化和部署” 是一种非常普遍和有效的工作流。开发者利用动态图的灵活性快速迭代模型,然后在部署时通过转换工具将其编译为高性能的静态图,从而兼顾开发效率和运行性能。


pytorch 的 torch.compile() 使用的是动态图还是静态图?

这是一个非常好的问题,它触及了 PyTorch 2.0 的核心。简短的回答是:

torch.compile() 在后台将您的动态图代码 编译 成一个高性能的静态子图,以实现最佳性能。 它本质上是一个 “动态图转静态图”的即时编译器

下面我来详细解释这个过程和原理。

核心思想:两全其美

torch.compile() 的设计哲学是:保留 PyTorch 动态图直观的开发和调试体验,同时在运行时自动获取静态图的性能优势。

它并不是将整个程序变成一个巨大的静态图,而是采用了一种更智能的方式。


工作原理:提取、编译、执行

torch.compile() 的工作流程可以概括为以下几步:

  1. 动态图追踪:当您第一次使用编译后的模型运行时,PyTorch 会在幕后执行您的代码,但并不实际进行计算。它只是“观察”或“追踪”在给定的输入下,执行了哪些算子以及它们之间的数据流。这产生了一个计算图。

  2. 生成静态子图:PyTorch 会将这个追踪到的计算图分解成一个或多个静态子图。这些子图是独立的、优化的计算单元。框架会尝试捕捉尽可能大的子图,以最大化优化范围。

  3. 深度优化:这是最关键的一步。PyTorch 将这个静态子图发送给一个强大的编译器后端(最常用的是 TorchInductor)。TorchInductor 会进行一系列激进的静态图优化,例如:

    • 算子融合:将多个相邻的算子(如 matmul + add + ReLU)融合成一个单一的核函数,极大地减少内存读写和内核启动开销。
    • 布局优化:改变张量在内存中的存储格式,以更好地适配硬件(如 GPU)。
    • 内核代码生成:为特定的子图生成高度优化的、定制化的 GPU 或 CPU 内核代码。
  4. 缓存与执行:优化后的内核代码被缓存起来。之后,当模型再次以相同形状的输入运行时,PyTorch 会直接调用这个高效的、编译好的内核,完全绕过 Python 解释器和动态图调度。

一个生动的类比

想象一个快递仓库:

  • 纯动态图:就像让一个工人(Python解释器)看着订单(你的代码),一次只拿一个包裹(一个算子),从仓库这头跑到那头。效率很低,因为来回跑动太多。
  • 纯静态图:就像在开工前,先花很长时间画出一张完美的、固定的包裹分拣路线图,然后让整个流水线按图执行。一旦画好,效率极高,但如果包裹类型变了,图就失效了。
  • torch.compile():像一个智能机器人。它先观察前几个订单(第一次运行),然后自动规划并构建一条最优的传送带(静态子图) 来处理这类订单。之后所有同类订单都走这条高效的传送带。如果订单模式变了(如图形改变),它会重新规划一条新的传送带。

动态图与静态图的角色

  • 对用户而言(接口层):动态图
    您仍然像以前一样编写和调试 PyTorch 代码。您可以随意使用 printpdb 断点,使用原生的 Python 控制流(if, for, while)。torch.compile() 是一个简单的装饰器,您只需包装一下模型即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import torch

    model = MyLargeModel().cuda()
    # 用法极其简单,用户体验仍是“动态图”
    compiled_model = torch.compile(model)

    # 第一次运行:会触发追踪和编译,可能有延迟
    output = compiled_model(input_data)

    # 后续运行:直接调用缓存的、优化后的内核,速度飞快!
    output2 = compiled_model(input_data2)
  • 对系统而言(执行层):静态图
    在后台,您的动态图操作被捕获并编译成了静态图内核。实际的硬件执行是在这个优化后的静态图上进行的,从而获得了接近纯静态图框架(如 TensorRT)的性能。

总结

特性 torch.compile() 的解决方案
开发体验 动态图。直观、灵活、易于调试。
运行时性能 静态图。通过即时编译成优化的静态子图来实现高性能。
工作原理 在后台动态地从您的代码中提取出静态子图并进行深度优化。
灵活性 极高。如果输入导致计算图改变(例如序列长度变化),它会自动重新编译一个新的静态图来适应,结合了二者的优点。

因此,torch.compile() 不是选择动态图或静态图,而是巧妙地融合了二者。它让 PyTorch 用户无需改变编码习惯,就能“鱼与熊掌兼得”,这使其成为大模型训练和推理中一个极具吸引力的工具。