Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

AI 编译器 / AI Compilers

1. AI 编译器全景——为什么模型需要编译器 / The AI Compiler Landscape and Why Models Need Compilers

2. 编译原理速通——面向 ML 工程师的核心概念 / Compiler Fundamentals for Machine Learning Engineers

3. 中间表示基础——理解 IR 层级与 lowering 链路 / Intermediate Representation Levels and Lowering Pipelines

4. 计算图的构建与表示 / Building and Representing Computational Graphs

5. MLIR 架构、方言与渐进式降级 / MLIR Architecture, Dialects, and Progressive Lowering

6. 算子语义、广播、归约与形状推导 / Operator Semantics, Broadcasting, Reduction, and Shape Inference

7. 模型前端格式:ONNX、TFLite、HLO 与 SavedModel / Model Frontend Formats: ONNX, TFLite, HLO, and SavedModel

8. 图优化 Pass——经典优化在 ML 中的应用 / Graph Optimization Passes for Machine Learning

9. 算子融合——编译器最重要的性能优化 / Operator Fusion as a Core Compiler Optimization

10. 内存规划——Buffer 分配与生命周期管理 / Memory Planning, Buffer Allocation, and Lifetime Management

11. Layout 优化——数据排布转换与内存效率 / Layout Optimization for Data Movement and Memory Efficiency

12. 动态 Shape——符号分析与形状处理 / Dynamic Shapes, Symbolic Analysis, and Shape Processing

13. 硬件约束下的操作调度 / Operation Scheduling Under Hardware Constraints

14. 从模板、DSL 到 IR 降级的代码生成架构 / Code Generation Architectures from Templates and DSLs to IR Lowering

15. CPU 后端:SIMD、分块与多线程 / CPU Backends with SIMD, Tiling, and Multithreading

16. CUDA 后端:合并访存与 Tensor Core / CUDA Backends, Memory Coalescing, and Tensor Cores

17. NPU 后端:脉动阵列与端侧 AI 生态 / NPU Backends, Systolic Arrays, and Edge AI Ecosystems

18. Kernel 性能基础:Roofline 与 Occupancy / Kernel Performance Fundamentals with Roofline and Occupancy

19. CUTLASS 与分层 GEMM 模板 / CUTLASS and Hierarchical GEMM Templates

20. TVM Tensor Expression 与计算调度分离 / TVM Tensor Expressions and Compute-Schedule Separation

21. 使用 Triton 编写高性能 GPU Kernel / Triton for High-Performance GPU Kernels in Python

22. 基于成本模型与实测搜索的自动调度 / Automatic Scheduling with Cost Models and Measurement-Based Search

23. XLA 内部机制:HLO、融合与 SPMD / XLA Internals, HLO, Fusion, and SPMD

24. Torch-MLIR:从 PyTorch 算子到 MLIR 方言 / Torch-MLIR from PyTorch Operators to MLIR Dialects

25. torch.compile:Dynamo、AOTAutograd、Inductor 与 Triton / Torch Compile with Dynamo, AOTAutograd, Inductor, and Triton

26. 从 MLIR 经 LLVM 降级到机器码 / Lowering from MLIR Through LLVM to Machine Code

27. 量化——低精度推理的工程实践 / Engineering Low-Precision Inference with Quantization

28. 分布式编译与训练——多设备编排的编译器支持 / Compiler Support for Distributed Training and Multi-Device Orchestration

29. 生产调试——真实问题的编译器视角排查 / Production Debugging from the Compiler Perspective

30. 未来方向——AI 编译器的新挑战与机遇 / Future Challenges and Opportunities for AI Compilers

本页目录

📅 创建时间:2026-06-03 🏷️ 标签:#计算图 #DataflowGraph #动态图 #静态图 #TorchDynamo #FX #Tracing

📚 前置知识:[[/04-ai/01-llm-engineering/07-llm-evolution]](LLM 发展脉络) 📚 相关知识:[[02-ir-fundamentals]](IR 基础)[[04-mlir-architecture]](MLIR 架构)[[05-operation-semantics]](算子语义)


计算图的构建与表示 / Building and Representing Computational Graphs ​

┌──────────────────────────────────────────────────────────────────────────────┐
│  🎯 场景:torch.compile 的"图简化"困惑                                            │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  你写了一个带条件的 PyTorch 模型:                                              │
│                                                                              │
│  ```python                                                                   │
│  def forward(self, x):                                                        │
│      if x.sum() > 0:                                                         │
│          return self.layer1(x)                                               │
│      else:                                                                   │
│          return self.layer2(x)                                               │
│  ```                                                                         │
│                                                                              │
│  用 torch.compile 编译后,发现 torch.compile 捕获的图里没有 if/else 分支,          │
│  只有一个被"简化"过的版本。你开始怀疑:编译器是不是把我的模型改坏了?                 │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第1节:计算图基础——什么是节点,什么是边 ​

1.1 图的基本定义 ​

计算图(Computational Graph) 是深度学习框架表示神经网络计算的核心数据结构。在数学上,它是一个有向无环图(DAG),其中:

元素数学定义在深度学习中的含义
节点(Node)图中的顶点算子(Operation/Tensor Expression)
边(Edge)连接节点的线张量(Tensor)流向
入度(In-degree)指向节点的边数算子的输入数量
出度(Out-degree)从节点发出的边数算子的输出数量
python
# 用伪代码展示一个简单计算图的构建过程
class Tensor:
    """张量节点"""
    def __init__(self, name, shape, dtype):
        self.name = name          # 节点标识
        self.shape = shape        # 张量形状
        self.dtype = dtype        # 数据类型
        self.producer = None     # 生产这个张量的算子
        self.consumers = []       # 消费这个张量的算子列表

class Operation:
    """算子节点"""
    def __init__(self, name, op_type):
        self.name = name          # 算子名称
        self.op_type = op_type    # 算子类型:matmul, relu, add...
        self.inputs = []          # 输入张量列表
        self.outputs = []         # 输出张量列表

# 构建一个简单的计算图:y = ReLU(Wx + b)
# 对应的计算图:
#   x (输入) ──→ MatMul ──→ Add(b) ──→ ReLU ──→ y (输出)
#   W (权重) ──↗

def build_simple_graph():
    """
    展示如何手动构建一个计算图
    """
    # 创建张量节点
    x = Tensor("x", shape=(32, 128), dtype="float32")      # 输入张量
    W = Tensor("W", shape=(128, 64), dtype="float32")     # 权重矩阵
    b = Tensor("b", shape=(64,), dtype="float32")          # 偏置向量
    
    # 创建算子节点
    matmul = Operation("matmul", "MatMul")
    matmul.inputs = [x, W]
    
    add = Operation("add", "Add")
    add.inputs = [matmul.outputs[0] if matmul.outputs else None, b]  # 需要 matmul 输出
    
    relu = Operation("relu", "ReLU")
    relu.inputs = [add.outputs[0] if add.outputs else None]
    
    # 建立边关系:生产者-消费者
    matmul.outputs = [Tensor("matmul_out", shape=(32, 64), dtype="float32")]
    matmul.outputs[0].producer = matmul
    
    add.outputs = [Tensor("add_out", shape=(32, 64), dtype="float32")]
    add.outputs[0].producer = add
    
    relu.outputs = [Tensor("y", shape=(32, 64), dtype="float32")]
    relu.outputs[0].producer = relu
    
    # 建立消费者关系
    matmul.outputs[0].consumers.append(add)
    add.outputs[0].consumers.append(relu)
    
    print(f"计算图拓扑:{x.name} → {matmul.name} → {add.name} → {relu.name} → {relu.outputs[0].name}")
    # 输出:计算图拓扑:x → matmul → add → relu → y
    
    return [matmul, add, relu]

build_simple_graph()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

1.2 数据流 vs 控制流 ​

计算图有两种基本的执行模式,理解它们的区别对于理解编译器的行为至关重要:

数据流图(Dataflow Graph) 是深度学习框架最常用的表示方式。在数据流图中:

  • 边的存在表示数据依赖
  • 节点执行顺序由数据依赖决定
  • 没有"控制流"——所有条件分支都通过数据选择实现
python
"""
数据流图的思维实验:
原始代码(带控制流):
    if condition:
        y = layer1(x)
    else:
        y = layer2(x)

数据流图等价表示(无控制流):
    # 用 Mask 选择分支输出
    mask = condition.cast(dtype)              # True → 1.0, False → 0.0
    y = mask * layer1(x) + (1 - mask) * layer2(x)
"""

# 实际 PyTorch 中的条件分支编译结果(简化示意)
def dataflow_equivalence():
    """
    展示条件分支如何被编译成数据流图
    
    原始 Python 代码:
        if x.sum() > 0:
            return self.layer1(x)
        else:
            return self.layer2(x)
    
    编译器捕获的可能等价表示:
    """
    # 1. 计算两个分支(两路都执行)
    out1 = "layer1_output"   # 分支1的结果
    out2 = "layer2_output"   # 分支2的结果
    
    # 2. 计算条件
    condition = "x.sum() > 0"  # 布尔条件
    
    # 3. 用选择操作代替 if/else
    # out = tf.where(condition, out1, out2)  ← 典型的数据流表示
    
    print("条件分支的数据流等价表示:")
    print("    mask = (x.sum() > 0) ? 1.0 : 0.0")
    print("    y = mask * layer1(x) + (1-mask) * layer2(x)")
    print()
    print("⚠️ 代价:两个分支都会被执行,只是结果被 mask 选择了")

dataflow_equivalence()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

第2节:动态图 vs 静态图——Eager 模式的优势和代价 ​

2.1 动态图(Dynamic Graph / Eager Execution) ​

动态图模式下,Python 代码被逐行解释执行。每行代码立即执行并返回结果,不需要事先构建计算图。

python
# PyTorch 动态图示例
import torch

class DynamicGraphDemo(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = torch.nn.Linear(128, 64)
    
    def forward(self, x):
        # 每次调用 forward,图都是新构建的
        # 输入形状可以任意变化
        
        # 第1次调用:x.shape = (32, 128)
        # 第2次调用:x.shape = (16, 128)  ← 不同的 batch size
        
        h = self.linear(x)
        h = torch.relu(h)
        
        # 可以在运行时添加任意 Python 控制流
        if h.sum() > 0:
            h = h * 0.5  # 动态分支
        
        return h

# 每次调用都重新构建图
model = DynamicGraphDemo()
output1 = model(torch.randn(32, 128))  # 32 个样本
output2 = model(torch.randn(16, 128))  # 16 个样本,batch size 不同
output3 = model(torch.randn(8, 128))    # 8 个样本

print(f"动态图优势:支持任意形状输入,Python 控制流完全保留")
print(f"output1.shape = {output1.shape}")  # torch.Size([32, 64])
print(f"output2.shape = {output2.shape}")  # torch.Size([16, 64])
print(f"output3.shape = {output3.shape}")  # torch.Size([8, 64])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

动态图的优势:

优势说明
调试友好可以在任意位置 print、检查变量、和 IDE 集成
灵活性支持任意 Python 控制流、数据结构
无 Shape 限制每次输入可以是不同形状
快速迭代无需编译阶段,适合研究和实验

动态图的代价:

代价说明
运行开销每次前向传播都要解释 Python 代码
优化受限编译器无法看到完整的计算图
部署困难需要运行时环境,难以序列化

2.2 静态图(Static Graph / Graph Execution) ​

静态图模式下,Python 代码首先被"捕获"成一个计算图,然后图被编译和优化,最后编译后的图被执行。

python
"""
TensorFlow 1.x 静态图示例(伪代码)

# 阶段1:定义计算图(不执行任何操作)
with tf.Graph().as_default():
    # 这里的代码只是构建图结构,不会实际执行
    x = tf.placeholder(tf.float32, shape=[None, 128])
    W = tf.Variable(tf.random_normal([128, 64]))
    b = tf.Variable(tf.zeros([64]))
    
    # 矩阵乘法操作被添加到图中
    h = tf.matmul(x, W) + b
    y = tf.nn.relu(h)
    
    # 定义优化操作
    loss = tf.reduce_mean((y - y_target) ** 2)
    train_op = tf.train.AdamOptimizer(0.001).minimize(loss)
    
    # 阶段2:编译和优化(这里才真正优化图)
    # 编译过程会做:
    #   - 常量折叠
    #   - 算子融合
    #   - 内存优化
    #   - 设备放置
    
    # 阶段3:执行(session.run)
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for _ in range(1000):
            sess.run(train_op, feed_dict={x: batch_x, y_target: batch_y})
"""

print("静态图优势:编译优化、执行效率高、易于部署")
print()
print("静态图劣势:")
print("  - 调试困难(错误发生在图执行时,不在 Python 代码处)")
print("  - 灵活性差(不支持动态 Shape、控制流受限)")
print("  - 编译时间长(图越大,编译越慢)")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

2.3 两种模式的对比 ​

特性动态图(PyTorch)静态图(TF1.x)
执行模式解释执行先编译后执行
调试逐行调试,IDE 完全支持需要 tfdbg,困难
动态控制流✅ 完全支持❌ 有限支持
动态 Shape✅ 完全支持❌ 固定 Shape
执行效率较低(解释开销)较高(优化后)
部署难度较难(需要 TorchScript)较易(已编译)
编译时间无需编译较长
内存使用动态管理可优化

第3节:Graph Capture 策略——编译器如何捕获计算图 ​

3.1 Tracing(追踪)策略 ​

Tracing 是最直观的图捕获方法:执行一次函数,记录所有操作。

python
import torch
import torch.nn.functional as F

# ============================================================
# PyTorch FX (torch.fx) Tracing 示例
# ============================================================

class SimpleModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear1 = torch.nn.Linear(128, 64)
        self.linear2 = torch.nn.Linear(64, 32)
    
    def forward(self, x):
        h = self.linear1(x)
        h = F.relu(h)
        h = self.linear2(h)
        return h

model = SimpleModel()

# 用 torch.fx 进行 symbolic tracing
# 关键点:函数会被"执行一次",执行过程中的操作被记录下来
traced = torch.fx.symbolic_trace(model)

print("=" * 60)
print("Tracing 捕获的计算图:")
print("=" * 60)
traced.graph.print_tabular()
# 输出示例:
# opcode         name    target                     args        kwargs
# -------------  ------  -------------------------  ----------  --------
# placeholder    x       x                          ()          {}
# call_module    linear1  linear1                    (x,)        {}
# call_function  relu    <built-in method relu...>  (linear1,)  {}
# call_module    linear2  linear2                    (relu,)     {}
# output         output  output                     (linear2,)  {}

print()
print("生成的 Python 代码:")
print(traced.code)
# 输出示例:
# def forward(self, x):
#     linear1 = self.linear1(x)
#     relu = torch.relu(linear1)
#     linear2 = self.linear2(relu)
#     return linear2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

Tracing 的核心问题:无法捕获控制流

python
# ============================================================
# Tracing 无法捕获控制流的例子
# ============================================================

class ModelWithControlFlow(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = torch.nn.Linear(128, 64)
        self.layer2 = torch.nn.Linear(128, 64)
    
    def forward(self, x):
        # 动态控制流:取决于输入值
        if x.sum() > 0:
            return self.layer1(x)
        else:
            return self.layer2(x)

model_with_control = ModelWithControlFlow()

# tracing 只会捕获执行时走的分支
# 如果 x.sum() > 0,则只捕获 layer1 分支
traced_control = torch.fx.symbolic_trace(model_with_control)

print("控制流模型的 Tracing 结果:")
print(traced_control.code)
# 只会得到其中一个分支!另一个分支在图中完全消失了!

print()
print("⚠️ 警告:Tracing 的局限性")
print("=" * 60)
print("原始代码有两个分支:")
print("    if x.sum() > 0:")
print("        return self.layer1(x)  ← 分支 A")
print("    else:")
print("        return self.layer2(x)  ← 分支 B")
print()
print("Tracing 结果(假设 x.sum() > 0):")
print("    return self.layer1(x)  ← 只有分支 A,B 完全丢失!")
print()
print("解决方案:")
print("  1. 使用 make_fx 或 random kwargs 强制走不同分支")
print("  2. 使用 TorchScript 解析 AST")
print("  3. 使用 wrapper 函数包装分支选择")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

3.2 AST Parsing(抽象语法树解析)策略 ​

AST Parsing 直接解析 Python 源代码,不执行函数,因此可以保留控制流结构。

python
# ============================================================
# TorchScript AST 解析示例
# ============================================================

"""
TorchScript 的工作原理:
1. 分析 Python 函数的 AST
2. 将 Python 语法转换为 TorchScript IR
3. TorchScript IR 支持控制流原语(if, while, for)

原始 Python:
    def forward(self, x):
        if x.sum() > 0:
            return self.layer1(x)
        else:
            return self.layer2(x)

TorchScript IR(简化表示):
    graph(%self, %x):
        %cond = aten::sum(%x)
        %cmp = aten::gt(%cond, 0)
        %result: Tensor = prim::If(%cmp):
            block0():
                %out1 = aten::linear(%x, %self.layer1.weight, %self.layer1.bias)
                -> (%out1)
            block1():
                %out2 = aten::linear(%x, %self.layer2.weight, %self.layer2.bias)
                -> (%out2)
        return (%result)
"""

# TorchScript 支持 if/while/for 等控制流
class TorchScriptControlFlow(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = torch.nn.Linear(128, 64)
        self.layer2 = torch.nn.Linear(128, 64)
    
    def forward(self, x):
        # @torch.jit.script 会保留这个 if 结构
        if x.sum() > 0:
            return self.layer1(x)
        else:
            return self.layer2(x)

# 使用 TorchScript 编译
scripted_model = torch.jit.script(TorchScriptControlFlow())

print("TorchScript 保留控制流的 IR:")
print(scripted_model.graph)
# 输出包含 prim::If 节点,带有两个 block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

3.3 Symbolic(符号化)策略 ​

Symbolic 方法使用抽象解释(Abstract Interpretation):不执行具体操作,而是跟踪值的"形状"和"类型"。

python
# ============================================================
# JAX Symbolic / Jaxpr 示例
# ============================================================

import jax
import jax.numpy as jnp

def simple_function(x, W, b):
    """JAX 函数"""
    h = jnp.matmul(x, W) + b
    return jnp.relu(h)

# 用 jax.make_jaxpr 生成符号化的中间表示
x = jnp.ones((32, 128))
W = jnp.ones((128, 64))
b = jnp.ones((64,))

# make_jaxpr 返回 Jaxpr( JAX 程序表示)
jaxpr = jax.make_jaxpr(simple_function)(x, W, b)

print("JAX Symbolic 中间表示(Jaxpr):")
print(jaxpr)
# 输出:
# { lambda ; a:i32[32,128] b:i32[128,64] c:i32[64].
#   let d:i32[32,64] = broadcast_in_dim[
#       broadcast_dimensions=(1, 2)
#       shape=(32, 64)
#     ] c
#       in let e:i32[32,64] = matmul_general[dimension_numbers=((1,),(0,),(0, 1),)]
#           a b
#           f:i32[32,64] = add e d
#           g:i32[32,64] = relu f
#       in { a }

print()
print("Symbolic 方法的特点:")
print("  - 保留数据类型和形状信息(i32 = int32)")
print("  - 操作被标记为抽象的(broadcast_in_dim, matmul_general)")
print("  - 可以进行符号化优化(常量折叠、代数简化)")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

3.4 三种策略对比 ​

特性TracingAST ParsingSymbolic
工作原理执行并记录解析源码抽象解释
控制流❌ 无法保留✅ 完整保留⚠️ 部分保留
副作用❌ 会被执行❌ 会执行❌ 不会执行
执行速度快(只执行一次)快(不执行)快(抽象执行)
动态 Shape⚠️ 受限✅ 支持✅ 支持
实现复杂度低高高
适用场景大部分情况复杂控制流数学表达式
代表框架torch.compile, FXTorchScriptJAX, Relay (TVM)

第4节:PyTorch FX 实战 ​

4.1 FX 基本用法 ​

python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.fx as fx

# ============================================================
# 完整的 FX Workflow
# ============================================================

class FeatureExtractor(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        self.conv2 = nn.Conv2d(64, 128, 3, padding=1)
        self.pool = nn.MaxPool2d(2)
    
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.pool(x)
        x = F.relu(self.conv2(x))
        x = self.pool(x)
        return x

class Classifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(128 * 8 * 8, 256)
        self.fc2 = nn.Linear(256, 10)
    
    def forward(self, x):
        x = x.view(x.size(0), -1)  # Flatten
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

class FullModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = FeatureExtractor()
        self.classifier = Classifier()
    
    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

# 步骤1:创建模型
model = FullModel()
model.eval()  # FX tracing 需要 eval 模式

# 步骤2:Symbolic Tracing
# 创建假的输入来指导 tracing
example_input = torch.randn(1, 3, 32, 32)
traced_model = fx.symbolic_trace(model)

print("=" * 60)
print("FX 捕获的计算图:")
print("=" * 60)
traced_model.graph.print_tabular()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

4.2 手动图操作 ​

python
# ============================================================
# FX 图操作:手动插入、删除、替换节点
# ============================================================

class ConvBatchNormFusion(nn.Module):
    """演示如何手动融合 Conv + BatchNorm"""
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 64, 3, padding=1)
        self.bn = nn.BatchNorm2d(64)
    
    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return x

model = ConvBatchNormFusion()
traced = fx.symbolic_trace(model)

# 图操作需要获取 Graph 对象
graph = traced.graph

print("原始图结构:")
for node in graph.nodes:
    print(f"  {node.op}: {node.name} = {node.target}")

# 手动遍历图找到 Conv 和 BatchNorm 节点
def fuse_conv_bn(graph_module):
    """
    融合 Conv + BatchNorm 的简化实现
    真实实现要复杂得多,这里演示核心思路
    """
    graph = graph_module.graph
    
    # 遍历所有节点
    for node in list(graph.nodes):
        if node.op == 'call_module' and isinstance(graph_module.get_submodule(node.target), nn.BatchNorm2d):
            # 找到 BatchNorm 节点,检查它的输入
            bn_node = node
            conv_node = None
            
            # 向上遍历找 Conv
            for inp in bn_node.all_input_nodes:
                if inp.op == 'call_module':
                    conv_node = inp
                    break
            
            if conv_node is not None:
                print(f"  发现 Fusion 候选: {conv_node.name} -> {bn_node.name}")
                # 实际融合:修改权重、删除 BN 节点
                # 这里简化处理,真实实现需要:
                # 1. 计算融合后的 BatchNorm 参数
                # 2. 更新 Conv 的权重和偏置
                # 3. 将 BN 的后续节点指向 Conv

fuse_conv_bn(traced)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

4.3 使用 Proxy 进行调试 ​

python
# ============================================================
# FX Proxy 机制:允许在 tracing 时插入打印
# ============================================================

def register_hooks(module, prefix=''):
    """为模块的每个子模块注册前向钩子"""
    for name, child in module.named_children():
        full_name = f"{prefix}.{name}" if prefix else name
        def forward_hook(mod, input, output, name=full_name):
            print(f"  [HOOK] {name}: input shape = {input[0].shape}, output shape = {output.shape}")
        child.register_forward_hook(forward_hook)
        register_hooks(child, full_name)

class DebugModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear1 = nn.Linear(128, 64)
        self.linear2 = nn.Linear(64, 32)
    
    def forward(self, x):
        h = self.linear1(x)
        print(f"  [DEBUG] After linear1: shape={h.shape}, mean={h.mean().item():.4f}")
        h = F.relu(h)
        h = self.linear2(h)
        print(f"  [DEBUG] After linear2: shape={h.shape}, mean={h.mean().item():.4f}")
        return h

# 注册钩子
register_hooks(DebugModel())

# tracing 时钩子不会生效(因为是符号执行)
# 但我们可以手动遍历
model = DebugModel()
traced = fx.symbolic_trace(model)

# 打印所有节点
print("调试追踪:手动遍历节点")
for node in traced.graph.nodes:
    if node.op == 'call_module':
        print(f"  节点: {node.name}, 目标: {node.target}")
        print(f"    输入: {[inp.name for inp in node.all_input_nodes]}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

第5节:动态图的特殊挑战 ​

5.1 Python 特定语法的处理 ​

python
# ============================================================
# 动态图挑战1:print 语句
# ============================================================

class PrintDebugModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(128, 64)
    
    def forward(self, x):
        # print 在 Python 中是语句,不是表达式
        print(f"Input shape: {x.shape}")
        return self.linear(x)

# FX 无法处理 print(不在 forward 的返回值中)
try:
    traced = fx.symbolic_trace(PrintDebugModel())
except Exception as e:
    print(f"⚠️ FX 无法捕获 print:{e}")

# 解决方案1:使用 assert
class PrintFixModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(128, 64)
    
    def forward(self, x):
        # 用 assert 代替 print(assert 也是语句)
        assert x.shape[-1] == 128, f"Expected last dim 128, got {x.shape[-1]}"
        return self.linear(x)

# 解决方案2:使用 wrapper
def wrap_with_print(forward_fn):
    """包装函数,添加打印逻辑"""
    def wrapped(*args, **kwargs):
        result = forward_fn(*args, **kwargs)
        return result
    return wrapped

# ============================================================
# 动态图挑战2:字典和集合
# ============================================================

class DictModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.weights = {'a': nn.Linear(128, 64), 'b': nn.Linear(128, 64)}
    
    def forward(self, x, key):
        # 字典索引是动态的
        return self.weights[key](x)

# FX 无法捕获字典索引(需要静态分析 key 的所有可能值)
try:
    traced = fx.symbolic_trace(DictModel())
except Exception as e:
    print(f"⚠️ FX 无法捕获字典索引:{e}")

# 解决方案:使用 nn.ModuleList 或 nn.ModuleDict
class FixedDictModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.weights = nn.ModuleDict({
            'a': nn.Linear(128, 64),
            'b': nn.Linear(128, 64)
        })
    
    def forward(self, x, key):
        return self.weights[key](x)

# 这样 FX 可以捕获(ModuleDict 有确定性索引)
traced = fx.symbolic_trace(FixedDictModel())
print("✓ ModuleDict 可以被 FX 捕获")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

5.2 数据依赖的条件执行 ​

python
# ============================================================
# 动态图挑战3:数据依赖的条件
# ============================================================

class DataDependentCondition(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 32)
        )
    
    def forward(self, x):
        # 条件依赖于 x 的值!这使得静态分析困难
        threshold = x.mean()  # 动态计算的阈值
        if threshold > 0:
            return self.net(x)
        else:
            return x

# FX tracing 会执行一次函数
# 假设 x.mean() > 0,则只捕获 then 分支
traced = fx.symbolic_trace(DataDependentCondition())

print("⚠️ Data Dependent Condition 的 Tracing 结果:")
print(traced.code)
# 只有一个分支!另一个分支完全消失!

print()
print("⚠️ 运行时问题:")
print("  当 threshold <= 0 时,模型会执行未追踪的分支 B")
print("  可能导致:")
print("    1. 运行时错误(形状不匹配)")
print("    2. 性能问题(重新走 Python 解释器)")
print("    3. 数值问题(梯度无法正确计算)")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

5.3 副作用的处理 ​

python
# ============================================================
# 动态图挑战4:副作用
# ============================================================

class SideEffectModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.counter = 0  # 模型内部状态
    
    def forward(self, x):
        self.counter += 1  # 修改内部状态!
        print(f"Forward pass #{self.counter}")
        return x * self.counter

model = SideEffectModel()

# Tracing 时 counter 被记录为常量 0
traced = fx.symbolic_trace(model)

print("副作用问题演示:")
print("  原始模型:每次调用 counter 增加")
print("  Traced 模型:counter 被固定为 0(静态值)")
print()
print("  原始模型调用 3 次:")
for i in range(3):
    result = model(torch.ones(1, 8))
    print(f"    Pass {i+1}: counter = {model.counter}")

print()
print("  Traced 模型调用 3 次:")
for i in range(3):
    result = traced(torch.ones(1, 8))
    print(f"    Pass {i+1}: 无副作用")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

第6节:图的等价性——编译器捕获的图和原始代码语义等价吗 ​

6.1 等价性的定义 ​

计算图的语义等价性有几种不同的定义:

类型定义检查方法
结构等价节点和边完全相同简单图同构
计算等价相同输入产生相同输出随机测试
渐近等价输出在数值误差内相同浮点比较
语义等价包括副作用的等价需要规范定义

6.2 常见的不等价情况 ​

python
# ============================================================
# 图捕获可能引入的不等价情况
# ============================================================

print("=" * 60)
print("图捕获可能导致不等价的几种情况:")
print("=" * 60)

print()
print("1️⃣ 控制流丢失")
print("   原始:if x.sum() > 0: return A else: return B")
print("   Traced:return A  (假设 x.sum() > 0 为 True)")
print()
print("2️⃣ 数值误差累积")
print("   静态图可能做常数折叠:(W1 @ W2) @ x = W1 @ (W2 @ x)")
print("   浮点运算顺序不同可能导致数值差异")
print()
print("3️⃣ 副作用丢失")
print("   原始:x = layer(x); print(x.sum())")
print("   Traced:x = layer(x)  (print 丢失)")
print()
print("4️⃣ 动态 Shape/dtype 丢失")
print("   原始:处理任意 batch size")
print("   Traced:固定 batch size = 1")
print()
print("5️⃣ 随机操作")
print("   原始:torch.randn()")
print("   Traced:固定随机种子")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

第7节:子图编译——哪些部分值得编译 ​

7.1 编译收益分析 ​

python
# ============================================================
# 子图编译策略
# ============================================================

def analyze_subgraph_benefits():
    """
    分析哪些子图值得编译
    """
    print("=" * 60)
    print("子图编译收益分析")
    print("=" * 60)
    
    scenarios = [
        ("小模型 (<1M 参数)", "低", "编译开销 > 执行收益"),
        ("大模型 (100M+ 参数)", "高", "执行时间长,编译一次收益多次"),
        ("计算密集型 (CNN/Transformer)", "高", "矩阵乘法可大量优化"),
        ("内存密集型 (大量小操作)", "中", "算子融合可减少内存访问"),
        ("IO 密集型 (数据加载瓶颈)", "低", "计算优化无意义"),
        ("动态控制流多", "低", "图捕获困难,收益不确定"),
    ]
    
    print()
    print("| 场景 | 编译收益 | 原因 |")
    print("|------|----------|------|")
    for scene, benefit, reason in scenarios:
        print(f"| {scene} | {benefit} | {reason} |")

analyze_subgraph_benefits()

print()
print("=" * 60)
print("子图编译策略")
print("=" * 60)

print("""
策略1:整图编译
    torch.compile(model)
    优点:全局优化
    缺点:编译慢,无法处理动态部分

策略2:部分编译(torch.compile 的 backend='aot')
    # 只编译部分模块
    compiled_layer = torch.compile(model.layer)
    
策略3:分离编译
    # 动态部分用解释器,静态部分用编译器
    def forward(self, x):
        if x.shape[0] < 32:  # 动态分支
            return self.fast_path(x)
        else:  # 静态分支
            return self.compiled_path(x)

策略4:JIT 编译(延迟编译)
    # 第一次执行时编译
    @torch.compile
    def my_function(x):
        return x @ x.T
""")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

7.2 TorchDynamo 的智能图捕获 ​

python
# ============================================================
# TorchDynamo 的自适应编译
# ============================================================

"""
TorchDynamo(torch.compile 的核心)是基于 Python Bytecode  interception 的图捕获系统:

工作原理:
1. 拦截 Python Bytecode(使用 eval_frame API)
2. 将 Python 代码翻译成 FX Graph
3. 对图进行优化
4. 生成优化后的 Python Bytecode

优势:
- 可以处理任意的 Python 代码
- 支持动态控制流(通过 guards)
- 支持动态 Shape
- 增量编译(只编译变化的图)

Guards 机制:
DYNAMO 会在编译时添加"守卫"(guards),用于检查运行时的条件:
- 输入 Shape
- 输入 dtype
- 模型参数是否改变
- 全局变量是否改变

如果 guards 失败,DYNAMO 会重新编译
"""

print("TorchDynamo 的图捕获流程:")
print("""
  Python 代码
      ↓
  Bytecode interception (eval_frame)
      ↓
  FX Graph construction (AC (Ahead-of-time) or AOT)
      ↓
  Optimization passes
      ↓
  Guard generation
      ↓
  Compiled Python Bytecode
      ↓
  Guard failure? → Re-compile
""")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

升华 ​

┌──────────────────────────────────────────────────────────────────────────────┐
│  📚 计算图表示:核心原则                                                       │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. **图是程序的中间表示**:计算图是连接高级语言和硬件指令的桥梁                 │
│                                                                              │
│  2. **动态 vs 静态是权衡**:动态图灵活但慢,静态图快但不灵活                    │
│                                                                              │
│  3. **图捕获策略决定能力边界**:Tracing 简单但不支持控制流                      │
│                                                                              │
│  4. **等价性需要验证**:编译器捕获的图不一定和原始代码语义等价                   │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

"AI 可查 vs 必须理解"清单 ​

必须理解(不理解就等于不会):

  • 🔴 计算图的基本概念:节点=算子,边=张量,这是所有深度学习框架的基础
  • 🔴 动态图 vs 静态图的权衡:为什么 PyTorch 选择动态图,TensorFlow 为什么转向 eager 模式
  • 🔴 Tracing 的局限性:为什么 Tracing 无法捕获控制流,替代方案是什么
  • 🔴 图捕获的三种策略:Tracing、AST Parsing、Symbolic 的适用场景
  • 🔴 FX 的基本用法:symbolic_trace 能做什么,不能做什么

AI 可查(知道去哪查就行):

  • ✅ 具体框架的 API 细节:torch.fx 的 API 变化很快,可以查文档
  • ✅ 特定模型的图结构:用 model.graph.print_tabular() 查看
  • ✅ TorchDynamo 的 guards 机制:内部实现复杂,用到时查文档
  • ✅ 特定编译优化的效果:benchmark 数据,不同硬件差异很大
  • ✅ 子图编译的边界情况:特定模型可能有特殊问题

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇3. 中间表示基础——理解 IR 层级与 lowering 链路 / Intermediate Representation Levels and Lowering Pipelines
下一篇5. MLIR 架构、方言与渐进式降级 / MLIR Architecture, Dialects, and Progressive Lowering

持续记录,持续成长

Copyright © Tidenflow