一、算子融合概述

1.1 什么是算子融合

算子融合,说白了就是把多个连续的计算操作合并成一个。你想想看,深度学习模型里经常有 Conv + BN + ReLU 这种固定搭配,原本要分三步走,现在一步到位。

我刚开始接触这个概念时,觉得不就是把代码写在一起嘛,有什么难的?后来才发现,这里面的门道深着呢。算子融合不是简单的代码合并,而是从计算图层面,把多个算子节点合并成一个更大的算子节点。

举个例子,一个典型的融合模式:

// 融合前:三个独立的算子
y1 = Conv2D(x, w)      // 卷积
y2 = BatchNorm(y1)     // 批归一化  
y3 = ReLU(y2)          // 激活函数

// 融合后:一个融合算子
y = ConvBnReLU(x, w)   // 一步到位

嗯,这里要注意,不是所有算子都能随便融合。得满足两个条件:一是计算上有依赖关系,二是融合后不会改变数学结果。

1.2 为什么需要算子融合

这个问题我问过不少新人,答案五花八门。其实核心原因就三个字:效率

我在项目中遇到过这样一个场景:一个 ResNet-50 模型,原始计算图有将近 200 个算子。每次推理都要反复启动内核、搬运数据。你想想看,这得浪费多少时间?

具体来说,不融合的代价体现在三个方面:

  • 内存带宽瓶颈:每个算子都要读写中间结果,数据在内存和计算单元之间来回倒腾
  • 内核启动开销:GPU 上每个 kernel launch 都有固定延迟,算子越多延迟越大
  • 计算密度低:小算子计算量小,无法充分利用硬件资源

核心观点:算子融合的本质,是用计算换访存。多算一点,少搬一次,往往能带来数倍的性能提升。

1.3 算子融合的收益分析

咱们来具体算算账。我习惯用三个维度来衡量融合收益:

1.3.1 减少访存

这是最直接的收益。不融合时,每个算子的输出都要写回显存,下一个算子再读进来。融合后,中间结果直接留在寄存器或共享内存里。

场景 访存次数 数据量(假设 4MB)
不融合(Conv + BN + ReLU) 6 次(3读3写) 24 MB
融合后 2 次(1读1写) 8 MB

看到没?访存量直接减少 2/3。我在优化一个 MobileNet 模型时,光靠算子融合就把内存带宽占用从 85% 降到了 40%。

1.3.2 减少 kernel launch 开销

GPU 上启动一个 kernel 大概需要 5-10 微秒的开销。听起来不多?但一个模型可能有几百个算子,累积起来就相当可观了。

我曾经调试过一个线上服务,发现 GPU 利用率只有 30%,但延迟却很高。后来一查,原来是 kernel launch 太频繁,GPU 大部分时间都在等指令。融合后,kernel 数量从 200 多个降到了 50 个左右,延迟直接砍半。

避坑指南:我曾经以为融合越多越好,结果发现过度融合会导致寄存器压力过大,反而降低性能。合适的融合粒度需要根据具体硬件来调。

1.3.3 提升计算密度

计算密度 = 计算量 / 访存量。这个比值越高,硬件利用率就越好。

小算子的计算密度通常很低,比如一个简单的 element-wise add,计算量很小,但访存却不少。融合后,多个小算子合并成一个,计算量叠加,访存不变,计算密度自然就上去了。

我习惯用一个简单的公式来估算:

计算密度提升 ≈ (融合前总计算量) / (融合前总访存量) 
                ÷ 
                (融合后总计算量) / (融合后总访存量)

嗯,这个公式虽然粗糙,但用来做初步评估足够了。

1.4 知识体系总览

下面这张图是我自己总结的算子融合知识框架,你可以对照着看:

算子融合知识体系 为什么需要算子融合? 内存带宽瓶颈 Kernel Launch开销 计算密度低下 算子融合策略 横向融合 纵向融合 跨层融合 条件融合 收益:减少访存 + 降低开销 + 提升计算密度

这张图把算子融合的来龙去脉都串起来了。从问题出发,到策略选择,再到具体收益,每一步都有清晰的逻辑关系。

重要提醒:算子融合不是银弹。有些场景下,融合反而会降低灵活性,增加编译时间。我建议你在做融合优化前,先用 profiler 跑一遍,找到真正的瓶颈在哪里。

好了,这一章的内容就到这里。记住一句话:算子融合的核心,就是用计算换访存。后面我们会深入具体的融合策略和实现细节。

专注资料整理