算子融合策略:Conv+BatchNorm+ReLU融合

算子融合,说白了就是把多个连续的小算子合并成一个。

为什么要这么做?因为每个算子执行时,都要读写显存。你想想看,数据在显存里搬来搬去,时间都花在I/O上了。我刚开始做推理优化时,就吃过这个亏——模型跑得慢,以为是计算瓶颈,结果一分析,发现大部分时间都在等数据。

Conv+BatchNorm+ReLU 融合原理

这三兄弟经常一起出现。卷积做完,接个BatchNorm归一化,再来个ReLU激活。典型的CNN标配。

融合的核心思路很简单:把BatchNorm的参数合并到卷积的权重里。

BatchNorm的数学公式是:

y = gamma * (x - mean) / sqrt(var + eps) + beta

而卷积的输出是:

z = W * x + b

把BatchNorm代入卷积输出:

y = gamma * (W * x + b - mean) / sqrt(var + eps) + beta

整理一下,就变成了新的卷积:

W' = gamma * W / sqrt(var + eps)
b' = gamma * (b - mean) / sqrt(var + eps) + beta

嗯,这里要注意:融合后的卷积,权重和偏置都变了。我在项目中遇到过有人直接拿原始权重去跑融合后的模型,结果精度全乱了。

核心要点:融合后的卷积层,已经包含了BatchNorm的归一化效果。推理时不需要再单独计算BatchNorm。

融合后的计算图变化

融合前,计算图长这样:

Input → Conv → BatchNorm → ReLU → Output

融合后,变成了:

Input → ConvReLU → Output

你看,三个节点变成了一个。显存访问次数从3次降到了1次。我做过实测,融合后推理速度能提升20%-40%,具体看模型大小和硬件。

这里我画了一张图,帮你理解这个过程:

融合前: Input Conv BatchNorm ReLU Output 融合后: Input ConvReLU (融合后) Output 节点数:5 → 3 显存访问:3次 → 1次 速度提升:约20%-40%

我的经验:融合后的模型,显存带宽占用明显降低。如果你用NVIDIA的Nsight工具看,会发现kernel启动次数也少了。这对小batch推理尤其友好。

使用TVM/Fuser自动融合

手动融合太累了。尤其是模型层数多的时候,一个一个改权重,容易出错。

我推荐用TVM或者TensorRT的自动融合工具。它们会在编译阶段自动识别可融合的算子。

TVM的融合方式

TVM里有个叫FuseOps的pass。它会遍历计算图,找到Conv+BatchNorm+ReLU这种模式,然后自动替换。

# TVM自动融合示例
import tvm
from tvm import relay

# 构建一个简单的网络
x = relay.var("x", shape=(1, 3, 224, 224))
w = relay.var("w", shape=(64, 3, 3, 3))
b = relay.var("b", shape=(64,))

# Conv
conv = relay.nn.conv2d(x, w, strides=(1,1), padding=(1,1))
# BatchNorm
gamma = relay.var("gamma", shape=(64,))
beta = relay.var("beta", shape=(64,))
mean = relay.var("mean", shape=(64,))
var = relay.var("var", shape=(64,))
bn = relay.nn.batch_norm(conv, gamma, beta, mean, var)
# ReLU
relu_out = relay.nn.relu(bn[0])

# 编译时自动融合
mod = tvm.IRModule.from_expr(relu_out)
with tvm.transform.PassContext(opt_level=3):
    mod = relay.transform.FuseOps()(mod)

print("融合后的计算图:")
print(mod)

你看,代码里我们只定义了Conv、BN、ReLU三个算子。TVM在编译时自动把它们合并成了一个。

避坑指南:我曾经遇到过TVM融合后精度下降的问题。排查了半天,发现是BatchNorm的epsilon参数没对齐。TVM默认的eps是1e-5,但我的模型训练时用的是1e-3。所以,融合前一定要检查超参数是否一致。

TensorRT的融合方式

TensorRT更激进。它不光融合Conv+BN+ReLU,还会把Add、Bias这些也融进去。

// TensorRT C++ 示例
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
nvinfer1::INetworkDefinition* network = builder->createNetwork();

// 添加卷积层
auto conv = network->addConvolution(*input, 64, DimsHW{3,3}, *weights, *bias);
// 添加激活层
auto relu = network->addActivation(*conv->getOutput(0), ActivationType::kRELU);

// 构建engine时自动融合
builder->setMaxBatchSize(1);
builder->setMaxWorkspaceSize(1 << 30);
auto engine = builder->buildCudaEngine(*network);

TensorRT在build engine时,会自动做算子融合。你不需要手动指定任何参数。

融合后的性能对比

模型 融合前(ms) 融合后(ms) 提升比例
ResNet-50 12.3 8.7 29.3%
MobileNetV2 8.1 5.9 27.2%
EfficientNet-B0 15.6 11.2 28.2%

数据是在T4 GPU上测的,batch size=1。你看,提升都在25%以上。

总结一下:Conv+BatchNorm+ReLU融合,是算子优化里性价比最高的操作。改动小,收益大。我建议你在做推理优化时,第一个就做这个融合。

嗯,今天就聊到这儿。融合后的模型,显存占用更少,推理更快。你回去试试看,应该能感受到明显的差异。

可可精品资料库