在繁忙的食堂后厨里,大厨做番茄炒蛋要过三道工位:一号工位洗番茄,二号工位切块,三号工位打蛋。麻烦的是,每道工位做完都要把半成品装进保鲜盒、放回储物柜,下一道工位再从柜子里取出来。一盘菜还没下锅,大厨已经来回开了五六次柜门。深度学习推理也一样:模型是一张计算图,每个算子(卷积、批归一化、激活、矩阵乘……)就像一道工位,执行完要把中间结果写回内存,下一个算子再读回来。算子融合(Operator Fusion / Kernel Fusion),就是把这些相邻工位合并成一个“料理台”:洗好直接切,切好直接下锅,中间结果在台面上交接,不再来回存取。本文用这个厨房比喻,把融合原理、收益分析、Conv-BN-ReLU 与 MatMul-Add 两种经典融合、手写与编译器自动融合、性能评估和融合策略一网打尽,让“推理加速”不再神秘。

岗位关联

对 AI 从业者而言,掌握算子融合意味着真正理解推理引擎的加速原理——从“会部署模型”进阶到“能优化模型运行时”,这是模型部署、推理引擎开发与端侧加速岗位的核心竞争力。

目录
  1. 可视化演示
  2. 融合基本原理
  3. 融合收益分析
  4. Conv-BN-ReLU融合
  5. MatMul-Add融合
  6. 手写融合算子
  7. 编译器自动融合
  8. 性能评估方法
  9. 融合策略设计
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

融合基本原理

大厨做番茄炒蛋时,一号工位洗番茄、二号工位切块、三号工位打蛋,每一道做完都要把半成品装进保鲜盒放回储物柜,下一道再取出来。深度学习推理正是这个场景:模型是一张计算图,每个节点是一个算子——卷积、批归一化、激活、矩阵乘法等等。每个算子由推理引擎启动一个内核(Kernel)来执行,输入输出都放在全局内存中。相邻算子之间,必然发生一次“上一个算子写中间结果,下一个算子读中间结果”的往返。

算子融合(Operator Fusion)就是把这些相邻算子合并成一个内核,让中间结果留在寄存器或片上缓存里直接交接,不经过全局内存。这是推理引擎中最常用、也最直观的一种图优化手段。融合后的执行图节点变少,每个节点干的活变多,但数据搬运和调度开销显著下降。

如果我们把一次推理的总时间写出来,就能看清融合省在哪里:

回到厨房:融合就像把洗、切、打蛋三个工位并成一个料理台,半成品在台面上直接交接,柜门一次都不用开。理解了这一点,后面所有收益分析和融合模式都是在这个“少开柜门”的道理上展开的。

T推理=∑k=1N(tk启动+tk计算+tk访存+tk同步)T_{\text{推理}} = \sum_{k=1}^{N} \bigl( t_k^{\text{启动}} + t_k^{\text{计算}} + t_k^{\text{访存}} + t_k^{\text{同步}} \bigr)
N 是内核个数,融合通过减小 N、并直接砍掉相邻算子之间的访存项来降低总时间。
  1. 总时间分解端到端推理延迟等于所有内核延迟之和,每个内核的时间是四项开销累加
    T推理=∑k=1N(tk启动+tk计算+tk访存+tk同步)T_{\text{推理}} = \sum_{k=1}^{N} \bigl( t_k^{\text{启动}} + t_k^{\text{计算}} + t_k^{\text{访存}} + t_k^{\text{同步}} \bigr)
  2. 启动开销每次调用内核都有固定启动延迟,N 越小则启动总开销越小,这是融合的直接收益来源
    tk启动t_k^{\text{启动}}
  3. 访存开销读写全局内存最贵,融合让中间结果不落内存,相当于从求和中去掉若干无效访存项
    tk访存t_k^{\text{访存}}
  4. 求和意义N 从 3 变成 1,启动与同步项变为原来的三分之一,中间访存项直接消失
    ∑k=1N⟶∑k=11\sum_{k=1}^{N} \longrightarrow \sum_{k=1}^{1}

融合收益分析

厨房里决定两个工位是否合并,要看“存取储物柜的时间”和“真正动手处理的时间”谁占大头。如果大半时间都耗在开柜取放上,合并收益自然大;如果本来处理就很慢,合并也快不了多少。计算机里衡量这个比例的标准叫算术强度(Arithmetic Intensity)。

融合收益不是玄学,是可以量化的:先用 profiler 数出融合前后“内核数量”和“中间张量字节数”,再乘以硬件实测的内存带宽与内核启动延迟,就能算出理论加速。对访存密集的小算子组合,融合往往能带来 1.5~3 倍的端到端提升;对本来就计算密集的大算子,收益则有限。这就是为什么先分析再动手,比盲目合并更靠谱——毕竟省柜门时间的前提,是柜门时间真的占了大头。

I=FLOPsBytesI = \frac{\text{FLOPs}}{\text{Bytes}}
FLOPs 是算子完成的浮点运算次数,Bytes 是它读写的数据量。I 越低,说明算子越“访存密集”,融合收益越可观。
  1. 计算量FLOPs 即浮点运算次数,代表“动手处理”的总工作量
    FLOPs\text{FLOPs}
  2. 访存量Bytes 是内核从全局内存读出和写回的字节总数,代表“存取储物柜”的流量
    Bytes\text{Bytes}
  3. 比值含义I 低时,算子的瓶颈在内存带宽而不是运算单元,少搬数据就能快,所以融合收益大
    I=FLOPsBytes≪平衡点I = \frac{\text{FLOPs}}{\text{Bytes}} \ll \text{平衡点}
  4. 收益量化融合三项收益逐条可测——中间访存字节数下降、内核启动次数下降(N-1 次启动被省掉)、缓存局部性提升(数据连续被复用)
    ΔT=ΔT访存+(N−1) t启动+ΔT缓存\Delta T = \Delta T_{\text{访存}} + (N-1)\, t_{\text{启动}} + \Delta T_{\text{缓存}}

Conv-BN-ReLU融合

洗菜、切菜、焯水是三道工序。焯水要用盐水,但大厨可以在切菜时就把盐撒在番茄块上,后面直接下锅,省掉“取盐、配盐水”这一步。这就是参数折叠的思想。在神经网络里,卷积后面几乎总跟着批归一化(Batch Normalization)和 ReLU 激活,三者形影不离,是融合的头号目标。

训练阶段,BN 用每个批次的统计量做归一化;推理时统计量已固定,BN 就变成了一个纯粹逐通道的线性变换。于是我们可以把它直接折叠进前面卷积的权重和偏置里,再让 ReLU 紧随其后,三个算子合成一个内核。

实际部署中,TensorRT、ONNX Runtime、NCNN 等引擎都会自动完成这个折叠。折叠后卷积核和偏置各多一份“预调整”后的副本,参数总量不变,运算却少了两整个内核的数据往返。就像把盐提前撒在番茄块上,下锅那一瞬间,所有准备都已完成。

y_c = \frac{x_c - \mu_c}{\sqrt{\sigma_c^2 + \varepsilon}} \cdot \gamma_c + \beta_c\quad\Longrightarrow\quadw_c' = w_c \cdot \frac{\gamma_c}{\sqrt{\sigma_c^2 + \varepsilon}},\;\;b_c' = (b_c - \mu_c) \cdot \frac{\gamma_c}{\sqrt{\sigma_c^2 + \varepsilon}} + \beta_c
左边是推理时 BN 的固定变换,右边是折叠后的新卷积核与新偏置。数学上两者完全等价,但右边让 BN 在运行时彻底消失。
  1. BN 参数含义μ_c 与 σ_c² 是训练阶段统计出的逐通道均值与方差,推理时冻结为常数;γ_c 与 β_c 是可学习的缩放和平移参数
    yc=xc−μcσc2+ε⋅γc+βcy_c = \frac{x_c - \mu_c}{\sqrt{\sigma_c^2 + \varepsilon}} \cdot \gamma_c + \beta_c
  2. 防零常数ε 是一个极小正数,避免除零,通常在 1e-5 量级
    ε≈1×10−5\varepsilon \approx 1\times10^{-5}
  3. 缩放系数令 s_c 表示该通道的折叠缩放因子,折叠后的卷积核等于原卷积核逐通道乘以 s_c
    sc=γcσc2+ε,wc′=wc⋅scs_c = \frac{\gamma_c}{\sqrt{\sigma_c^2 + \varepsilon}},\quad w_c' = w_c \cdot s_c
  4. 偏置折叠原偏置先减均值再乘缩放,最后加 β,得到新偏置,把 BN 的平移吸收进来
    bc′=(bc−μc)⋅sc+βcb_c' = (b_c - \mu_c) \cdot s_c + \beta_c
  5. 等价替换Conv→BN→ReLU 替换为“用 w'、b' 做卷积 + ReLU”,两者输出逐元素数值等价,但内核数从 3 变 1
    Conv(x;w′,b′)→ReLUy\text{Conv}(x; w', b') \xrightarrow{\text{ReLU}} y

MatMul-Add融合

调好的酱汁直接淋在菜上是一步,先盛出来放旁边、上桌前再浇一遍,就是两步。线性层(全连接)、注意力打分、Transformer 的投影层,几乎都是“矩阵乘完马上加偏置”的结构,即 GEMM 后紧跟 Add。未融合时,A×B 的结果矩阵被完整写回显存,Add 内核再读出来逐元素加 bias,白白多了一次矩阵规模的写与读。

融合的做法很聪明:不需要额外内核,只要在矩阵乘的“尾声”(epilogue)阶段,把累加器的初始值设成 bias_j。乘加循环结束后每个输出元素天然带着偏置,直接写回一次即可。

这个融合模式在 Transformer 类模型里收益尤其明显:一个 512×512 的中间矩阵写回再读回,就是 2MB 的显存流量被省掉,而注意力头越多、序列越长,节省越可观。省掉的不仅是流量,还有 Add 内核的启动开销——酱汁不再需要先盛出来放旁边了。

Cij=∑k=1KAik Bkj+biasjC_{ij} = \sum_{k=1}^{K} A_{ik}\, B_{kj} + \text{bias}_j
累加器在乘加开始前就放入 bias_j,循环结束 C_ij 即为最终结果,中间矩阵从未产生过。
  1. 输出元素C_ij 表示输出矩阵第 i 行第 j 列的值,由 A 的第 i 行与 B 的第 j 列内积再加偏置得到
    CijC_{ij}
  2. 内积累加对 k 从 1 到 K 做乘累加,这是 GEMM 最核心的循环体
    ∑k=1KAik Bkj\sum_{k=1}^{K} A_{ik}\, B_{kj}
  3. 偏置广播bias_j 只与列号 j 相关,同一列的所有行共享同一个偏置值,逐元素相加即可
    + biasj+\ \text{bias}_j
  4. 融合实现将累加器寄存器初始化为 bias_j,循环结束自动得到带偏置结果,等价于数学上先求和后加 bias
    accij←biasj;accij+=AikBkj\text{acc}_{ij} \gets \text{bias}_j;\quad \text{acc}_{ij} \mathrel{+}= A_{ik} B_{kj}

手写融合算子

有的大厨不信任自动厨具,非要自己设计整套连贯动作:自己写菜谱、自己拿捏火候、自己决定什么时候该把菜铲到哪个锅里。手写融合算子也是这样——直接写一个 CUDA 内核,把卷积主循环、折叠后的权重、ReLU 激活全部写进同一个 kernel。它的核心技巧是循环合并(Loop Fusion):原本三个算子的三层循环,合并成一个外层滑动窗口循环,内层只做一次乘累加、一次激活、一次写回。

线程块设计上,每个线程负责计算一个输出像素,输入 patch 预先加载到共享内存或寄存器里反复复用;手工调优则围绕 unroll 次数、block 尺寸、寄存器上限进行。要理解手写内核的优化空间,先要知道卷积本身的计算量有多大:

计算量摆在这里,融合能做的不是减少它,而是让每一次运算都“吃得到”数据——输入 patch 从共享内存取、输出写回一次、激活在寄存器里顺手完成。手写融合的代价是开发成本高、可移植性差,换一个硬件平台可能就要重写。但它能榨出编译器自动融合漏掉的最后一点性能,这也是很多芯片厂商为旗舰模型定制融合内核的原因。

FLOPsconv=2×Hout×Wout×Cin×Cout×K2\text{FLOPs}_{\text{conv}} = 2 \times H_{\text{out}} \times W_{\text{out}} \times C_{\text{in}} \times C_{\text{out}} \times K^2
卷积的浮点运算量由输出尺寸、通道数与卷积核面积共同决定。计算量越大,越值得把访存环节打磨干净。
  1. 乘加计数一次乘和一次加共 2 个 FLOP,所以前面乘上 2
    22
  2. 输出空间H_out × W_out 是输出特征图的空间尺寸,每个输出像素都要独立算一轮
    Hout×WoutH_{\text{out}} \times W_{\text{out}}
  3. 通道与核C_in × C_out × K² 表示每个输出像素要对所有输入通道的 K×K 窗口做乘累加
    Cin×Cout×K2C_{\text{in}} \times C_{\text{out}} \times K^2
  4. 总运算量四个维度乘起来就是一次卷积的全部 FLOPs,它是硬性计算成本,融合不改变它
    FLOPsconv=2×HO×WO×CI×CO×K2\text{FLOPs}_{\text{conv}} = 2 \times H O \times W O \times C I \times C O \times K^2

编译器自动融合

智能厨具拿到整套菜谱后,会自动识别“洗→切→焯水”这类固定套路,把它合并成一个预设程序,大厨什么都不用改。编译器自动融合就是这套智能厨具:推理引擎内置图优化 Pass,遍历计算图,用模式匹配识别出 Conv→BN→ReLU、MatMul→Add 这类子图,整体替换成一个融合算子节点。

TVM 的 FuseOps Pass、TensorRT 的 layer fusion、ONNX Runtime 的 graph optimization、NCNN/MNN 的算子合并,都是这个套路。替换之后,调度器(auto-scheduler 或人工模板)决定融合后的内核如何分块、如何映射到硬件。自动融合依赖一个代价模型来评估“这个融合到底值不值”:

编译器自动融合的价值在于“零侵入”:模型代码一行不改,导入引擎后由图优化自动完成。代价是它对非常规子图的识别能力有限,且自动生成的代码有时不如手写内核极致。实际工程中,常见做法是“编译器自动融合打底,热点子图手写内核补刀”——智能厨具负责日常,大厨亲自出马处理招牌菜。

Cost=t启动+max⁡ ⁣(FLOPsP峰值, BytesB带宽)\text{Cost} = t_{\text{启动}} + \max\!\left( \frac{\text{FLOPs}}{P_{\text{峰值}}},\, \frac{\text{Bytes}}{B_{\text{带宽}}} \right)
融合后的内核执行时间,由启动开销加上“计算耗时与访存耗时中的较大者”组成。融合主要压缩的是启动项和 Bytes 项。
  1. 启动开销t_启动 是内核启动的固定延迟,融合后内核数减少,这项被压缩
    t启动t_{\text{启动}}
  2. 计算耗时FLOPs 除以硬件峰值算力 P_峰值,得到理想计算时间
    FLOPsP峰值\frac{\text{FLOPs}}{P_{\text{峰值}}}
  3. 访存耗时Bytes 除以有效内存带宽 B_带宽,得到理想访存时间,融合削减中间张量后这一项下降明显
    BytesB带宽\frac{\text{Bytes}}{B_{\text{带宽}}}
  4. 取较大者真实内核受更快到达瓶颈的一方约束,代价模型因此取二者最大值
    max⁡ ⁣(FLOPsP峰值, BytesB带宽)\max\!\left( \frac{\text{FLOPs}}{P_{\text{峰值}}},\, \frac{\text{Bytes}}{B_{\text{带宽}}} \right)

性能评估方法

新工序好不好,不能只看单步快不快,要看整桌菜的平均出菜时间、高峰客流的稳定性、占用台面空间。评估融合效果同样要全面:端到端延迟是最核心指标,但只报一次最快值没有意义,要取多次采样的 p50/p99 稳定值,并做 warm-up 消除冷启动影响。

除了延迟,还要看吞吐量(每秒处理多少张图)、峰值内存(融合后中间张量减少通常带来显存下降)、以及内核数量与带宽利用率的 profiler 数据。不同硬件平台上融合表现可能完全不同,A/B 对比要在目标设备上跑。加速比是最直观的定量指标:

评估不是一次性的:融合策略修改后要回归测试,量化部署后还要再测一遍,因为低精度推理的访存行为会变。就像大厨换了个新灶台,招牌菜的味道必须重新试吃确认——性能数据就是推理引擎的“试吃报告”。

S=T未融合T融合S = \frac{T_{\text{未融合}}}{T_{\text{融合}}}
S 大于 1 说明融合有效,S 越接近 1 越要谨慎,S 小于 1 说明融合反而拖慢了推理,应当回退。
  1. 基线延迟T_未融合 是优化前模型端到端平均延迟,作为对比基准
    T未融合T_{\text{未融合}}
  2. 优化延迟T_融合 是融合后同样的数据、同样的设备上测得的平均延迟
    T融合T_{\text{融合}}
  3. 加速比含义S 即“原来花几份时间,现在花一份时间”,1.5 表示提速 50%
    S=T未融合T融合S = \frac{T_{\text{未融合}}}{T_{\text{融合}}}
  4. 判定准则S>1 融合有效;S≈1 收益可忽略,需结合稳定性判断;S<1 说明发生了寄存器溢出或并行度下降,必须回退
    S>1⇒保留融合,S<1⇒回退S > 1 \Rightarrow \text{保留融合},\quad S < 1 \Rightarrow \text{回退}

融合策略设计

不是所有菜都适合“一锅出”:有的菜分锅炒更香,融得太狠反而糊锅。融合策略就是要回答“哪些算子该合、哪些该拆”。判断标准有四问:这个算子访存是否密集(算术强度远低于硬件平衡点)?中间结果是否没有其他分支依赖?融合后中间数据能否塞进寄存器或共享内存?融合会不会让并行度明显变差?

把这四问变成可计算的判据,就是代价模型。用省下的访存时间减去省下的启动时间作为融合净收益:

端侧场景还有额外约束:NPU 和移动 GPU 的片上缓存很小,融合粒度过大会导致中间数据溢出、回退到低速路径;超大算子(如大卷积核、超大 GEMM)则保留单算子,以利用专用的张量指令。融合策略设计的本质,是在“少开柜门”和“别把锅撑爆”之间找平衡——不是融得越多越好,而是融得恰到好处。

G=M消除B带宽−(N原−1)⋅t启动G = \frac{M_{\text{消除}}}{B_{\text{带宽}}} - (N_{\text{原}} - 1) \cdot t_{\text{启动}}
G 为融合净收益:第一项是消除的中间张量省下的访存时间,第二项是省下的内核启动时间,两者相减必须为正才值得融合。
  1. 消除的数据量M_消除 是融合后不再写回读回的中间张量总字节数
    M消除M_{\text{消除}}
  2. 省下的访存时间M_消除 除以有效内存带宽 B_带宽,即少搬这些数据省下的秒数
    M消除B带宽\frac{M_{\text{消除}}}{B_{\text{带宽}}}
  3. 省下的启动时间N_原 个内核融合成 1 个,省掉 N_原-1 次内核启动
    (N原−1)⋅t启动(N_{\text{原}} - 1) \cdot t_{\text{启动}}
  4. 净收益判定G>0 融合有净收益,G<0 则维持原计算图,避免负优化
    G=M消除B带宽−(N原−1)⋅t启动G = \frac{M_{\text{消除}}}{B_{\text{带宽}}} - (N_{\text{原}} - 1) \cdot t_{\text{启动}}

AI 应用场景

端侧手机 App 实时美颜加速

阶段:推理计算图优化(部署阶段)

解决的问题:老机型跑美颜模型每帧高达 30ms,瓶颈不是计算量,而是几十个小算子轮流启动、中间张量反复读写。传统做法直接换更强芯片,成本高且换代快。通过在 NCNN/MNN 推理引擎中开启算子融合,把 Conv-BN-ReLU 序列图结构自动合并,kernel 数从 40 降到 15,帧耗时降至 18ms,不动模型结构就解决了卡顿。

具体案例:某美颜 App 的人像分割网络由 MobileNet 骨干加 BN-ReLU 组成,部署工程师导出计算图后开启自动图优化,再对瓶颈子图手写融合内核,最终在骁龙 8 系手机上实现实时 30FPS。

自动驾驶域控上的 TensorRT 加速

阶段:车端 GPU 部署优化

解决的问题:目标检测模型含大量 Conv-BN-ReLU 与 MatMul-Add 组合,原始 ONNX 图直接跑 TensorRT 时 GPU 利用率低、延迟超时。传统优化靠人工改网络结构,风险高。利用 TensorRT 的自动 layer fusion 和 BN 参数折叠,端到端延迟从 12ms 降到 8ms,满足 30ms 帧预算的余量大幅提升。

具体案例:某 L2+ 自动驾驶方案的 3D 目标检测模型,部署工程师对比融合前后 profiler 数据,确认内核数从 280 个降至 96 个,显存峰值下降约 1.2GB,最终通过车规级延迟测试。

边缘 AI 摄像头 NPU 部署的融合策略

阶段:融合策略设计与代价模型评估

解决的问题:边缘盒子的 NPU 片上 SRAM 仅几 MB,盲目融合导致中间数据溢出触发 fallback,反而变慢。传统手工试错耗时且不可迁移。通过代价模型计算每个候选融合的净收益 G,并结合片上缓存容量挑选“半融合”方案,如只做 BN 折叠、不做跨层大融合,平衡性能与稳定性。

具体案例:某智慧园区摄像头部署 YOLOv5s,平台工程师在芯片 SDK 的图编译阶段配置融合白名单,依据算术强度与片上内存预算选融合项,最终推理帧率从 25FPS 提升到 40FPS,且无一次回退。

Python 演示

python63 行
import numpy as np  def conv2d(x, w, b):    """单通道二维卷积,stride=1,same 填充,b 为标量偏置。"""    H, W = x.shape    K = w.shape[0]    pad = K // 2    xp = np.pad(x, pad, mode='constant')    out = np.zeros((H, W))    for i in range(H):        for j in range(W):            out[i, j] = np.sum(xp[i:i + K, j:j + K] * w) + b    return out  def relu(x):    return np.maximum(x, 0.0)  def bn_infer(y, gamma, beta, mu, var, eps=1e-5):    """推理阶段的 BN:统计参数全部固定,只做线性变换。"""    return gamma * (y - mu) / np.sqrt(var + eps) + beta  def main():    rng = np.random.default_rng(42)    x = rng.normal(size=(8, 8))    w = rng.normal(size=(3, 3))    b = 0.2    gamma = 1.3    beta = -0.1    mu = 0.05    var = 1.1     # 未融合:conv -> bn -> relu,三个内核产生两块中间张量    y1 = conv2d(x, w, b)    y2 = bn_infer(y1, gamma, beta, mu, var)    y_unfused = relu(y2)     # 融合:先把 BN 折叠进卷积权重与偏置,再执行"卷积+ReLU"    scale = gamma / np.sqrt(var + 1e-5)    w_fused = w * scale    b_fused = (b - mu) * scale + beta    y_fused = relu(conv2d(x, w_fused, b_fused))     same = np.allclose(y_unfused, y_fused)    assert same, '融合前后结果不一致'     unfused_kernels = 3    fused_kernels = 1    unfused_mem_traffic = (y1.size + y2.size) * 4 * 2  # 两块中间张量各写回+读回    fused_mem_traffic = 0    saved = (1 - fused_mem_traffic / unfused_mem_traffic) * 100     print('融合前后输出一致:', same)    print('内核启动次数: %d -> %d' % (unfused_kernels, fused_kernels))    print('中间张量内存往返(字节): %d -> %d' % (unfused_mem_traffic, fused_mem_traffic))    print('访存节省: %.1f%%' % saved)  if __name__ == '__main__':    main()
目的:用 NumPy 完整实现 Conv-BN-ReLU 的推理期参数折叠,验证融合前后输出数值等价,并统计内核数与中间张量访存流量,直观展示融合收益。
  1. scale = gamma / np.sqrt(var + 1e-5)计算每个通道的折叠缩放系数,即 BN 参数折叠的核心一步
  2. w_fused = w * scale把 BN 的缩放吸收进卷积核,新卷积核直接可用
  3. b_fused = (b - mu) * scale + beta将均值和 β 平移吸收进偏置,BN 在运行时消失
  4. y_fused = relu(conv2d(x, w_fused, b_fused))融合后的计算路径只有“一次卷积 + 一次激活”
  5. assert same, '融合前后结果不一致'用断言强制保证数值等价,不满足则报错
  6. unfused_mem_traffic = (y1.size + y2.size) * 4 * 2统计未融合时两块中间张量的写回与读回总字节数
  7. out[i, j] = np.sum(...) + b卷积滑窗内的乘累加,模拟 GPU 内核的主循环

常见误区

误区我们部署时做了算子融合,模型的 FLOPs 变小了,所以推理才变快。

正解融合主要消除的是中间张量的写回与读回、以及内核启动次数,FLOPs 基本不变,个别情况甚至因边界填充略有增加。推理变快的真正来源是“省掉了装盒、存放、再取出”的访存与调度开销。用厨房比喻说:合并工位没有让切菜动作变少,而是省掉了来回开柜门的时间。

为何常见:人们习惯把“变快”直接归因于“算得更少”,忽略了内存搬运在推理中的真实占比,尤其是访存密集的小算子。

误区融合是数学变换,所以融合前后每个浮点数都应该一模一样,否则就是引擎有 bug。

正解数学上等价,但浮点运算次序被重排,卷积加 BN 变成单次乘加后,误差通常在 1e-6 量级,对精度影响可忽略。若应用对数值极其敏感,应在目标设备上做量化对比。等价性是“统计意义”上的,不是二进制 bit 级的一致。

为何常见:把“数学等价”误当成“浮点结果完全一致”,忽略了 IEEE 754 浮点运算不满足结合律这一事实。

误区计算图里挨着的两个算子,随手就能合到一起,融合器无脑合并就行。

正解合法融合要同时满足:两个算子输出形状一致、中间结果没有被其他分支引用、融合后保持数值等价、目标硬件指令支持该融合模式。编译器用依赖分析和模式匹配来判定,而不是看是否相邻。就像储物柜里的半成品可能有别的菜也要用,不能随便拿走。

为何常见:只看到拓扑相邻,忽略了数据依赖的多消费者问题,这是初学者设计融合策略时最常见的错误。

误区既然融合能省访存,那所有算子都该融合,越激进越快。

正解当算子本来就是计算密集、带宽充足时,融合省下的访存时间微乎其微;而融合后并行度下降、寄存器溢出导致 local memory 溢出反而更慢。成熟引擎都会用代价模型估算净收益 G,必要时回退。融合是“投资决策”,不是“无脑优惠券”。

为何常见:把“通常收益大”误当成“必然收益”,忽略了硬件瓶颈位置与资源约束的共同作用。

面试题

Q1. 算子融合最主要减少的是哪类开销?

单选题 · easy

  • A.模型参数量
  • B.中间内存访存← 正确答案
  • C.浮点计算总量
  • D.训练样本数量

答案:B。融合让中间结果留在寄存器或片上缓存,减少的是中间张量的写回与读回,以及内核启动次数。模型参数量和浮点计算总量基本不变,训练样本数量与推理优化无关。就像合并工位省的是开柜门时间,而不是切菜动作。

Q2. 推理阶段 BN 折叠进卷积的前提条件是什么?

单选题 · easy

  • A.训练时每次批量数据足够大
  • B.推理时 BN 统计参数已固定← 正确答案
  • C.卷积核尺寸必须是 1×1
  • D.输入图像必须是单通道灰度

答案:B。推理时 μ、σ² 来自训练集的固定统计量,γ、β 也是训练好的常数,BN 退化为线性变换,才能折叠进卷积。批量大小、卷积核尺寸、输入通道数都不是必要条件。

Q3. 算术强度 I = FLOPs/Bytes 很低,说明该算子最可能是?

单选题 · mid

  • A.计算密集型算子
  • B.访存密集型算子← 正确答案
  • C.通信密集型算子
  • D.并行度极高的算子

答案:B。算术强度低意味着每做一次浮点运算要搬很多字节数据,算子的天花板是内存带宽,属于访存密集型。这类算子融合收益最大,因为少搬中间数据就能直接提速。

Q4. Conv-BN-ReLU 融合后,新的卷积偏置 b' 的正确计算公式是?

单选题 · mid

  • A.只含原始偏置与均值项
  • B.(b-μ)×s+β 其中 s 为缩放系数← 正确答案
  • C.包含均值方差与 γ 项
  • D.仅包含缩放系数与偏置项

答案:B。令 s = γ/√(σ²+ε),折叠后的偏置 b' = (b-μ)×s+β,同时吸收均值平移、方差缩放和 β 平移。其他选项都漏掉了必须的平移或缩放项,数学上不成立。

Q5. 手写融合算子时,循环合并(Loop Fusion)的主要作用是?

单选题 · mid

  • A.减少嵌套循环的层数
  • B.让中间数据留在寄存器缓存← 正确答案
  • C.增大模型可训练参数量
  • D.压缩卷积核的存储大小

答案:B。循环合并把卷积、激活、写回放进同一循环体,中间结果在寄存器或共享内存中直接流转,不必写回全局内存再读回。层数减少只是表象,核心收益是数据就地复用。

Q6. 哪些情况适合作为算子融合候选?

多选题 · hard

  • A.算子相邻且输出形状一致← 正确答案
  • B.中间结果只被一个算子使用← 正确答案
  • C.融合后保持数值等价性← 正确答案
  • D.两个算子间存在分支依赖

答案:ABC。融合候选必须相邻、形状匹配、中间结果无其他消费者、数值等价。如果中间结果被多个分支使用,融合会导致重复计算或破坏数据流,因此 D 是反例。

Q7. 关于融合后加速比 S=T_未融合/T_融合 的说法,正确的是?

单选题 · hard

  • A.S 大于 1 时融合一定可部署
  • B.S 接近 1 时仍需评估稳定性← 正确答案
  • C.S 越小代表融合收益越大
  • D.S 与访存带宽完全无关

答案:B。S>1 只说明平均延迟下降,还需结合 p99、内存占用与硬件适配判断是否可部署;S 接近 1 时收益可忽略,要评估波动。S 越小收益越小,而访存带宽正是决定 S 的核心因素之一。

Q8. MatMul-Add 融合避免了哪些开销?

多选题 · easy

  • A.中间矩阵的写回显存← 正确答案
  • B.中间矩阵的读回计算← 正确答案
  • C.Add 算子的内核启动← 正确答案
  • D.矩阵乘法的计算总量

答案:ABC。融合把 bias 放进累加器初值,A×B 的中间矩阵从未写回显存,也就无需读回,Add 内核整体消失。但矩阵乘本身的乘加运算量一个都不少,D 错误。

Q9. 编译器自动融合时,代价模型通常考虑哪些因素?

多选题 · hard

  • A.内核启动时间开销← 正确答案
  • B.访存数据总量大小← 正确答案
  • C.硬件峰值算力与带宽← 正确答案
  • D.模型训练时的损失值

答案:ABC。代价模型用启动开销、FLOPs/峰值算力、Bytes/带宽三者估算内核耗时,判断融合是否值得。训练损失是训练阶段的概念,与推理期图优化无关。

Q10. 判断某算子是访存密集型并适合融合,最佳判据是?

单选题 · extreme

  • A.卷积核尺寸是否大于三
  • B.算术强度远低于硬件平衡点← 正确答案
  • C.输入输出张量是否超过十六
  • D.执行时间是否小于一毫秒

答案:B。Roofline 模型告诉我们,算术强度低于硬件平衡点(Peak FLOPs/带宽)时算子受带宽约束,属于访存密集,融合收益大。核尺寸、张量维度、绝对执行时间都是经验碎片,不能作为通用判据。