算子融合:提升推理效率的核心技术 - 第2节
一、计算图:深度学习模型的骨架
做推理加速这些年,我越来越觉得计算图就像模型的骨架。没有它,你根本不知道模型在干什么。
说白了,计算图就是一个有向无环图(DAG)。节点是算子,边是数据流动的方向。数据从输入节点流入,经过一系列算子处理,最终到达输出节点。
计算图的核心要素:
- 节点(Node):代表一个算子操作,比如卷积、ReLU、矩阵乘法
- 边(Edge):代表张量数据的流向,也就是中间结果
- 拓扑结构:决定了算子的执行顺序
举个例子,一个简单的两层神经网络:
# 伪代码展示计算图结构
input → Conv2D → ReLU → MaxPool → Conv2D → ReLU → Flatten → Dense → Softmax → output
每个箭头就是一条边,每个操作就是一个节点。数据从左到右流动,这就是前向传播。
我在项目中遇到过一个问题:同样的模型,在不同框架上跑出来的性能差很多。后来发现,是计算图的构建方式不同导致的。有的框架是静态图,有的框架是动态图,这直接影响后续的算子融合策略。
静态图 vs 动态图
| 特性 | 静态图 | 动态图 |
|---|---|---|
| 构建时机 | 执行前构建完整图 | 运行时逐行构建 |
| 优化空间 | 大,可全局优化 | 小,只能局部优化 |
| 调试难度 | 较难 | 容易 |
| 典型框架 | TensorFlow 1.x, ONNX | PyTorch, TensorFlow Eager |
我个人习惯用静态图做推理部署。为什么?因为静态图给了你全局优化的机会。你可以看到整个计算流程,然后决定哪些算子可以合并,哪些可以消除。
二、算子:计算图的最小执行单元
算子是什么?就是计算图里的每个节点。它定义了一个具体的数学运算。
我经常跟团队说:算子就是深度学习模型的「原子操作」。你想想看,不管多复杂的模型,拆到最底层,就是一堆算子的组合。
算子的分类
按功能来分,算子大致可以分为这几类:
- 计算密集型算子:卷积、矩阵乘法、全连接。这类算子计算量大,访存比高,是性能瓶颈的主要来源。
- 激活函数算子:ReLU、Sigmoid、Tanh。计算量不大,但数量多,频繁调用。
- 池化算子:MaxPool、AvgPool。用于降维,计算简单。
- 归一化算子:BatchNorm、LayerNorm。训练和推理行为不同,推理时通常可以融合。
- 形状变换算子:Reshape、Transpose、Squeeze。不涉及计算,只改变张量形状。
- 逐元素算子:Add、Mul、Div。两个张量对应元素做运算。
我的经验之谈:
做算子融合时,优先关注计算密集型算子和它周围的激活函数、归一化算子。这些组合的融合收益最大。我曾经把一个Conv+BN+ReLU融合成一个算子,推理速度提升了30%。
算子的属性
每个算子都有自己的一组属性。比如卷积算子:
Conv2D(
kernel_size=3, # 卷积核大小
stride=1, # 步长
padding='same', # 填充方式
dilation=1, # 膨胀系数
groups=1, # 分组数
in_channels=64, # 输入通道数
out_channels=128 # 输出通道数
)
这些属性决定了算子的计算量和内存访问模式。做融合时,必须考虑这些属性是否兼容。
三、计算图与算子的关系:整体与局部
计算图和算子的关系,说白了就是整体和局部的关系。计算图是宏观视角,算子是微观视角。
我画了一张图来展示它们的关系:
从这张图可以看得很清楚:计算图描述了数据流动的路径,而每个节点背后都是一个具体的算子实现。
为什么算子融合能加速?
原因其实很简单。你想想看,每个算子执行时,都要经历:
- 从内存读取输入数据
- 执行计算
- 把结果写回内存
如果两个算子分开执行,中间结果就要写回内存再读出来。这个读写操作的开销,有时候比计算本身还大。
算子融合的核心思想:
把多个连续的算子合并成一个,减少中间结果的读写次数。说白了,就是让数据在寄存器里多待一会儿,别老往内存里跑。
举个例子,Conv + ReLU 融合:
// 融合前:两次内存访问
conv_output = conv2d(input, weight) // 写回内存
output = relu(conv_output) // 从内存读取
// 融合后:一次内存访问
output = fused_conv_relu(input, weight) // 计算完直接输出
我曾经在一个项目中,把整个 ResNet-50 的 Conv+BN+ReLU 全部融合,推理速度提升了将近 40%。这就是算子融合的魅力。
计算图对算子融合的影响
计算图的结构决定了哪些算子可以融合。一般来说,满足以下条件的算子可以考虑融合:
- 连续执行:在计算图中是相邻节点
- 数据依赖:前一个算子的输出是后一个算子的唯一输入
- 计算模式匹配:比如卷积后面跟激活函数,这是最常见的融合模式
注意:
不是所有相邻算子都适合融合。比如 Conv + Softmax,这两个算子的计算模式差异太大,融合后反而可能变慢。我刚开始做融合时犯过这个错,后来学乖了,先做性能分析再决定。
嗯,到这里,计算图和算子的基本概念就讲完了。记住一句话:计算图是蓝图,算子是砖块。算子融合,就是把这些砖块砌得更紧凑,让数据流动得更快。
一句话总结:
理解计算图的结构,熟悉算子的特性,你才能知道哪些算子可以融合,哪些不能。这是做推理加速的第一步,也是最关键的一步。