一、算子融合概述

什么是算子融合

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

我打个比方你就明白了。假设你要做一道菜:先切菜,再洗菜,再炒菜。传统做法是切完放回碗里,洗完再放回碗里,最后再倒进锅里。算子融合就像——你切完直接在水龙头下冲,冲完直接下锅。省掉了中间倒腾的功夫。

在深度学习里,算子就是那些基本的计算单元,比如卷积、ReLU、BatchNorm、加法等等。模型推理时,这些算子一个接一个地执行。每个算子都要读写显存,这个开销其实很大。

核心思想:将多个连续算子合并为一个内核(kernel),减少中间数据的读写开销。

举个例子,一个典型的卷积+BN+ReLU模式:

// 未融合:三步走
conv_output = conv(input)      // 写显存
bn_output = batch_norm(conv_output)  // 读+写显存
output = relu(bn_output)       // 读+写显存

// 融合后:一步到位
output = fused_conv_bn_relu(input)  // 只读一次,只写一次

我在项目中遇到过,一个简单的ResNet-50模型,光这种Conv-BN-ReLU模式就有几十处。融合前后,推理速度能差30%以上。嗯,这个数字我记得很清楚。

为什么需要算子融合

你可能会问:不就是多读写几次显存吗?能有多大影响?

我跟你算笔账。GPU计算有个著名的「屋顶模型」(Roofline Model)。它告诉我们:

  • 计算密集型操作:算得快,瓶颈在计算能力
  • 访存密集型操作:算得慢,瓶颈在带宽

小算子恰恰属于后者。每个算子计算量不大,但都要读写显存。GPU的显存带宽虽然高,但跟计算速度比起来,还是差了好几个数量级。

操作 计算量 访存量 计算/访存比
Conv 3x3 高 中 高
ReLU 极低 高 极低
Elementwise Add 低 高 低

看到没?ReLU和加法这种操作,计算量几乎为零,但访存量跟卷积差不多。这就是典型的「算得少、搬得多」。

避坑指南:我曾经天真地以为,只要算子实现得好,性能就不会差。后来发现,很多时候瓶颈不在计算,而在数据搬运。算子融合就是解决这个问题的。

除了减少访存,算子融合还有几个好处:

  • 减少内核启动开销:每个算子都要启动一次GPU内核,启动本身就有延迟。融合后启动次数变少。
  • 提高缓存利用率:数据还在缓存里,下一个算子直接用,不用再去显存取。
  • 简化计算图:图更小,更容易优化。

算子融合的历史与发展

算子融合不是新鲜事。我入行那会儿,它就已经存在了。

最早期的深度学习框架,比如Caffe,其实没怎么考虑融合。那时候模型小,GPU也慢,瓶颈主要在计算上。后来模型越来越大,GPU越来越快,访存瓶颈就凸显出来了。

我记得大概是2016年左右,NVIDIA推出了cuDNN的融合API。那时候我还在做图像识别,第一次用上融合卷积,速度提升让我印象深刻。

发展脉络大致是这样的:

  • 手工融合时代(2015-2017):工程师手动写融合内核,比如Conv+ReLU、Conv+BN+ReLU。效率高但工作量大。
  • 编译器自动融合时代(2018-2020):TVM、XLA等编译器出现,能自动识别可融合的算子模式。我那时候参与过TVM的开发,说实话,自动融合的效果有时候不如手工,但胜在省力。
  • 图级别融合时代(2021-至今):不仅融合相邻算子,还能跨层融合。比如把整个残差块融合成一个内核。这需要更复杂的分析和调度。

下面这张图展示了算子融合的核心逻辑:

算子融合核心逻辑 输入数据 Conv 计算 写显存 BN 计算 写显存 输出数据 输入数据 融合内核 Conv + BN + ReLU 一次计算,一次写回 输出数据 未融合:3次读写 融合后:1次读写 节省 66%

现在的编译器,比如XLA和TVM,已经能自动做很多融合了。但说实话,自动融合还不够完美。有些复杂的融合模式,还是需要人工介入。

注意:不是所有算子都适合融合。融合后内核变复杂,寄存器压力增大,有时候反而会变慢。我踩过这个坑——把两个大卷积强行融合,结果寄存器溢出,性能反而下降了20%。

所以我的建议是:先用编译器自动融合,遇到瓶颈再手工调优。别一上来就手写融合内核,那是最后的手段。

好了,这一章就到这里。算子融合是个大话题,后面我们会深入讲各种融合策略和实现细节。

可可精品资料库