想象你在经营一家热闹的餐厅后厨。传统的烹饪流程是:洗菜师傅把菜洗好,装进盘子,端到切菜台;切菜师傅切好,又装进盘子,端到灶台;炒菜师傅炒完,再装进盘子,端到出餐口……每一步之间,菜都要被装进一个“盘子”(内存),由专人端到下一位师傅手里。如果餐厅生意火爆,你会立刻发现:大量时间浪费在“装盘、传菜、卸盘”上,师傅们真正动手烹饪的时间反而很少。 AI 编译器中的算子融合(Operator Fusion)技术,就是这位聪明的餐厅经理。它发现:既然洗菜、切菜、炒菜本来就在一条流水线上,为什么不让三位师傅肩并肩站在一起,菜直接从洗菜盆滑到切菜板、再滑进炒锅,省掉中间所有“装盘传菜”的环节呢?这种把数据流上相邻的算子串联为单个 kernel、让中间结果驻留在寄存器或共享内存中的优化技术,就是本文的主角——垂直融合(Vertical Fusion)。它让中间结果不再频繁写回全局内存,大幅减少了访存开销,是编译器优化中最核心的“省盘子”艺术。
对于 AI 从业者而言,理解垂直融合是读懂深度学习编译器(如 TVM、XLA)性能优化的钥匙,也是面试中区分“调参工程师”与“系统优化专家”的关键分水岭。
可视化演示
展示垂直融合将相邻算子串联为单kernel的数据流过程、融合窗口选择策略与垂直/水平融合对比,理解AI编译器核心优化技术。
本节配有交互动效(进入视口后自动加载,约 3KB)。
垂直融合数据流过程 融合前:MatMul→Bias→ReLU各自独立执行,中间结果写回HBM 融合后:三算子串联为单kernel,中间结果驻留寄存器 Gain = (T_global - T_fused) / T_global 消除中间内存读写
融合基础
前面说到,餐厅后厨的传菜环节浪费了大量时间。在 AI 芯片的世界里,这道“菜”就是计算产生的中间结果,而“传菜的盘子”就是全局内存(Global Memory),也叫 HBM(高带宽内存)。每当一个算子计算完毕,结果就要被“装盘”——写回全局内存;下一个算子开始前,又要把“菜”从盘子里取出来——从全局内存读取。这一写一读的访存操作,比计算本身慢上数百倍,是整个推理链路中最拖后腿的环节。
算子融合(Operator Fusion) 正是为此而生。它把数据流上相邻的多个算子合并成一个“超级算子”(mega-kernel),让中间结果在芯片内部的寄存器或共享内存中直接传递,省去了“装盘传菜”的往返。这种沿数据流方向、将前后串联的算子合并的方式,就是垂直融合(Vertical Fusion)。与之相对的是把同层、互不依赖的算子合并的水平融合(Horizontal Fusion),后续章节会详细对比。
垂直融合的核心洞察其实非常简单:访存是瓶颈,计算是便宜的。芯片的计算单元每秒能做几万亿次运算,但数据的搬运速度远远跟不上。正如餐厅里,师傅们切菜炒菜的速度极快,但传菜员在厨房里跑来跑去才是真正浪费时间的地方。垂直融合要做的,就是把这些“传菜员”从关键路径上撤掉。
回到厨房的比喻:如果一次请客要上十道菜,每道菜都由洗、切、炒三个师傅完成,传统的做法是每道菜都装盘传一次。垂直融合相当于让三位师傅组成一个“流水线小组”,第一道菜洗完直接滑到切菜板,切完直接进炒锅,十道菜下来省掉了二十次传菜。这就是它最朴素也最强大的威力。
- 算子序号N 表示数据流上相邻算子的总数,从 1 到 N 编号,如“洗菜→切菜→炒菜”三个步骤。
- 算子成本每个算子 i 的总成本由计算时间 Computeᵢ 和访存时间 MemoryAccessᵢ 构成。
- 访存主导近似由于访存比计算慢 1-2 个数量级,总成本可近似为所有访存时间之和,这揭示了优化的主攻方向。
数据流分析
既然要融合,首先得知道哪些算子能融合、哪些不能。这就好比餐厅经理要搞清楚:哪道菜必须等前面的工序完成才能开始?哪些工序之间可以无缝衔接?在编译器领域,这个摸底过程叫做数据流分析(Data Flow Analysis)。
数据流分析的核心是依赖关系识别。编译器会为每个算子建立一张“食材清单”(输入数据)和“菜品列表”(输出数据)。如果一个算子的输出恰好是下一个算子的输入,它们之间就存在生产者-消费者(Producer-Consumer)关系,这就是垂直融合的候选对象。就像切好的土豆丝恰好是炒菜师傅的原料,这两个工序天然适合肩并肩。
进一步地,编译器需要追踪中间结果的完整生命周期:从它被第一个算子“生产”出来,到被最后一个算子“消费”完毕。这个过程叫做访存轨迹追踪(Memory Access Tracing)。如果中间结果只被一个下游算子消费一次,那么融合后它就可以完全停留在寄存器中——这是最理想的融合场景。但如果它被多个算子共享,就需要仔细权衡是否值得融合。
数据流分析就像餐厅经理拿着“菜品流程图”,把每一道菜从洗菜到上桌的全过程画出来。通过这张图,经理能一眼看出哪些步骤是环环相扣的“流水线”,哪些是各自独立的“小作坊”。只有流水线上的工序才适合垂直融合,而独立的工序强行融合反而会打乱节奏。
- 融合判定函数Fusable(A, B) 是编译器判断算子 A 和 B 能否垂直融合的函数,返回 True 或 False。
- 输出输入交集Output(A) 是 A 产生的所有数据集合,Input(B) 是 B 需要的所有数据集合,∩ 表示求交集。
- 非空判据如果交集非空(≠∅),说明存在数据传递,返回 True 表示可以融合;否则返回 False。
融合窗口策略
知道了哪些算子可以融合,下一个问题是:一次融合多少个?这就像餐厅经理决定把多少道工序组成一个流水线小组。这个决策在编译器中被称为融合窗口(Fusion Window)策略,简单说就是一个“滑动窗口”在算子序列上滑动,每次圈定一批算子尝试融合。
窗口大小是其中最核心的权衡。窗口太大,一次融合的算子过多,中间结果虽然不再写回内存,但都要占着寄存器不放。而芯片的寄存器文件(Register File) 容量有限,就像厨房的操作台只有那么大,把所有食材都堆在上面会放不下,导致“溢出”到内存——这反而比不融合更慢。这就是寄存器压力(Register Pressure)预算问题。
编译器需要精打细算:融合后的超级算子在执行过程中,最多同时需要多少个寄存器来存放中间结果?这个数量必须小于芯片的寄存器总数。因此,窗口的选择本质上是一个约束优化问题:在寄存器容量允许的前提下,让融合的算子数量尽量多。
除了寄存器压力,融合窗口还会影响缓存局部性(Cache Locality)。窗口越大,数据被重复从内存读取的机会越少,缓存的命中率越高——就像流水线小组越长,菜在半成品状态下流转次数越多,越不需要从冷库重新取货。但过长又会导致操作台拥挤。编译器需要在这两者之间寻找甜蜜点。
- 目标函数Maximize W 表示优化目标是最大化融合窗口的大小,即一次融合尽量多的相邻算子。
- 约束条件subject to 引出约束,窗口内每个算子的寄存器需求量 Rᵢ 求和后必须小于等于芯片总寄存器数 R_total。
- 阈值平衡如果融合窗口超过寄存器容量,中间结果会被迫“溢出”到内存,反而引入新的访存开销,违背融合初衷。
水平协同组合
垂直融合并非孤军奋战。在真实的编译器中,它还有一个默契的搭档——水平融合(Horizontal Fusion)。如果说垂直融合是“前后串联的流水线合并”,那么水平融合就是“平行排列的工位合并”。
想象餐厅里有两位甜品师傅,分别在两个工作台做一模一样的提拉米苏。水平融合相当于把两个工作台合并成一个,让一位师傅同时对两份食材进行操作,共享配料和工具。在 AI 计算中,水平融合针对的是同层、共享输入或共享计算的算子,比如把两个独立的 element-wise 操作合并成一个,减少 kernel 启动次数。
实际优化中,编译器通常遵循先水平后垂直(Horizontal First, Then Vertical) 的策略。原因不难理解:先做水平融合,可以把同层的多个算子先“折叠”成少数几个;再做垂直融合,把这几层再“串联”成一条更紧凑的流水线。这就像先收拾桌面——把散落的工具归类放好——再规划流水线流程。
餐厅经理最终决定:先把所有甜品工位合并成一个“甜品岛”,再把甜品岛与主厨的灶台串联成“前菜-主菜-甜品”的无缝流水线。垂直融合与水平融合的协同,正是编译器优化中“两条腿走路”的智慧。
- 总节省率S_combined 表示两种融合组合后的总时间节省比例,取值范围在 0 到 1 之间。
- 互补关系(1 - S_horizontal) × (1 - S_vertical) 表示两者同时优化后剩余的时间比例,因为两次优化互不重叠。
- 节省叠加1 减去剩余比例即得到总节省率,体现“先水平后垂直”的乘法效应而非加法效应。
寄存器驻留
垂直融合最核心的收益,在于让中间结果驻留(Reside)在寄存器或共享内存中,而不写回全局内存。这需要深入理解芯片的存储层级。
在 GPU 等 AI 加速芯片中,存储系统是一个金字塔:最顶端是寄存器(Register),速度最快、容量最小,大约几十 KB;中间是共享内存(Shared Memory) 和一级缓存,速度次之;底层是全局内存 HBM,容量大但速度慢。数据离计算单元越近,访问越快。垂直融合的目标,就是让中间结果尽量留在金字塔的顶层。
实现寄存器驻留的关键在于中间结果传递机制。在融合后的超级算子中,每个算子从寄存器中读取输入,计算结果仍然写回寄存器,下一个算子直接从同一个寄存器读取——中间完全不触碰内存。这就像流水线上,洗好的菜直接滑到切菜板上,切好的菜直接推进炒锅,案板之间用滑槽连接,菜永远不会被端离这条线。
当然,寄存器驻留也有代价:寄存器是稀缺资源,驻留的数据越多,每个线程可用的寄存器越少,可能降低并行度。这再次回到融合窗口策略的权衡。但无论如何,让中间结果“住在”寄存器里,是垂直融合性能收益的最大来源,也是编译器优化中最诱人的一块蛋糕。
- 写回消除T_global_write 表示中间结果原本需要写回全局内存的时间,融合后这部分时间被完全消除。
- 读取消除T_global_read 表示下一个算子原本需要从全局内存读取该数据的时间,融合后这部分也被消除。
- 总节省两项相加即为每个中间结果省下的总时间。若数据被 k 个算子复用,节省时间还将成倍增加。
性能实战
理论讲得再好,不如用数据说话。在深度学习模型的实际部署中,垂直融合的收益可以用一个具体的例子来说明。
以 ResNet-50 中的一段典型计算为例:一个 Conv2D 卷积层后面紧跟着 BatchNorm 批归一化和 ReLU 激活函数。在未融合的朴素实现中,这三个算子依次执行:卷积结果写回 HBM,BatchNorm 读取再写回,ReLU 再读取再写回。每一步都涉及数百 MB 的数据搬运。而经过垂直融合后,三个算子合并为单个 kernel,卷积的结果直接进入寄存器,BatchNorm 和 ReLU 依次在相邻寄存器中完成,数据从不离开芯片内部。
访存延迟对比是衡量收益最直观的指标。在一次典型的推理任务中,融合前每个算子平均访存延迟约 120 个时钟周期,融合后下降到约 20 个时钟周期。这意味着,仅访存一项就节省了超过 80% 的时间。在端到端延迟上,垂直融合通常可以为推理模型带来 1.5x 到 3x 的加速,具体取决于模型中算子间的数据复用程度。
回到厨房的比喻:把洗、切、炒三个环节垂直融合后,原本做一道菜要跑三次操作台,现在只需要一条滑槽从头滑到尾。餐厅翻台率从每小时十桌提升到二十桌,这就是垂直融合在真实世界中的威力。理解这条优化路径,你就掌握了 AI 编译器性能优化的核心密码之一。
- 原始总时间T_original 表示融合前所有算子的总执行时间,包含每个算子的计算时间和独立的访存时间。
- 融合总时间T_fused 表示融合后的总时间,所有算子的计算时间相加,但访存只有一次共享开销 Memory_shared。
- 加速比两者相除得到加速倍数,数值越大说明融合带来的性能提升越显著。
AI 应用场景
大规模推理服务优化
阶段:模型部署与推理加速
解决的问题:在云端 GPU 集群上部署 GPT 等大语言模型时,推理延迟直接决定用户体验。传统逐算子执行方式中,每次 layer normalization、激活函数等操作都涉及全局内存读写,数以 GB 计的中间结果反复搬运,导致 GPU 利用率低下、单请求延迟高企。
具体案例:某大模型推理服务使用 TensorRT 编译器对 Transformer 的 LayerNorm + Residual Add + GeLU 进行垂直融合,将原本 4 个 kernel 合并为 1 个。优化后,单 token 生成延迟从 52ms 降至 18ms,GPU 利用率提升 45%,推理服务成本降低近一半。这正是垂直融合在后厨中“撤掉传菜员”的规模化实践。
边缘设备模型压缩
阶段:模型轻量化与端侧部署
解决的问题:手机、IoT 设备的算力和内存带宽非常有限,无法承受频繁的全局内存访问。传统方式在端侧部署模型时,每个算子的内存往返会快速耗尽带宽预算,导致发热和卡顿。
具体案例:某安防摄像头中部署人脸检测模型,通过将 Conv + BN + ReLU 三算子垂直融合为单个算子,中间结果全程驻留片上 SRAM,访存带宽占用减少 70%,模型在 4 核 ARM CPU 上首次达到实时处理(30 FPS)。垂直融合让端侧设备也能流畅运行 AI 模型。
Python 演示
"""Simple vertical fusion simulation. Simulate the memory access cost of fused vs unfused operators.""" class VerticalFusionSimulator: def __init__(self, operators, data_size=1024): # data_size: per-operator intermediate data size (KB) self.ops = operators self.data_size = data_size self.mem_latency = 200 # cycles per KB self.compute_latency = 20 # cycles per operator def unfused_cost(self): # Each op writes and reads its result from global memory total = 0 for _ in self.ops: total += self.compute_latency total += 2 * self.data_size * self.mem_latency return total def fused_cost(self): # Single kernel: compute all ops, only one memory write at the end total = len(self.ops) * self.compute_latency total += self.data_size * self.mem_latency return total def speedup(self): unfused = self.unfused_cost() fused = self.fused_cost() return unfused / fused def main(): ops = ["Conv2D", "BatchNorm", "ReLU", "Add"] sim = VerticalFusionSimulator(ops, data_size=1024) unfused = sim.unfused_cost() fused = sim.fused_cost() sp = sim.speedup() print("=== Vertical Fusion Cost Simulation ===") print(f"Operators: {ops}") print(f"Unfused total cost: {unfused} cycles") print(f"Fused total cost: {fused} cycles") print(f"Speedup: {sp:.2f}x") if __name__ == "__main__": main()
class VerticalFusionSimulator定义模拟器类,封装算子列表和数据量参数,类比“厨房流水线模拟器”。self.mem_latency = 200设置每 KB 数据的访存延迟为 200 周期,在实际硬件中全局内存延迟约为计算延迟的 10 倍以上。for _ in self.ops遍历每个算子,未融合时每个算子都独立产生一次完整的写回+读取开销。total += 2 * self.data_size * self.mem_latency一次写入加一次读取,共两次全局内存访问,这是融合要消除的“传菜”开销。fused_cost融合后所有算子共享一次访存,相当于把多个工序放在同一条流水线上,只需一次“装盘”。speedup计算加速比,即未融合总成本除以融合总成本,量化优化收益。
常见误区
误区我一开始觉得垂直融合就是把数据流上所有相邻的算子一股脑合并成一个大 kernel,反正合并得越多、内存访问越少,性能就越好。后来在工程里试着把十几个算子全合在一起,结果反而变慢了。
正解垂直融合必须在寄存器容量的约束下进行。融合后的算子需要同时占用的寄存器数量如果超过硬件上限,中间结果就会被“溢出”到本地内存,产生新的访存开销,得不偿失。就像厨房操作台只有两平米,硬把所有食材堆上去只会互相干扰,不如分两批做。
为何常见:这是最符合直觉的误解——“减少访存次数肯定更好”听起来天经地义,但实际上忽略了寄存器这一稀缺资源的存在。
误区我理解中水平融合和垂直融合都是优化访存的技术,那是不是用了垂直融合就不用水平融合了?或者它们在做同一件事,选一个就行?
正解两者解决的是完全不同的问题。垂直融合针对“前后串联”的算子数据流,消除中间结果的往返;水平融合针对“同层并行”的算子,减少 kernel 启动次数和共享计算。实际编译器通常先做水平融合精简算子数,再做垂直融合把剩余算子串成流水线,两者是互补协作关系。
为何常见:初学者容易把所有融合优化都看成一件事,忽略了它们在不同数据流维度上的差异化作用。
误区我拿到一个模型就想立刻开始融合加速,觉得只要算子挨着就能融合,不需要先做什么数据流分析。结果发现融合后的 kernel 计算结果和原来不一致,调试了半天。
正解垂直融合的前提是严格的数据流依赖分析。只有生产者算子的输出恰好被消费者算子直接使用,且没有其他算子共享该中间结果时,融合才是安全的。如果中间结果被多个算子复用,融合后可能会导致重复计算,破坏计算图语义。这就像流水线上每道菜只服务一条队,如果一道半成品菜要分给两个窗口,就不能强行合并。
为何常见:融合的“性能诱惑”太大,让人容易跳过严密的分析步骤,直奔“看起来更快”的方案。
误区我原以为把中间结果放在寄存器里就是纯赚,完全不理解为什么编译器不把能融合的算子全都融合了。寄存器空间多得很,放点中间结果有什么问题?
正解寄存器数量极其有限,一个 GPU 上每线程通常只有几十个寄存器。中间结果占据寄存器后,会降低线程占用率(occupancy),即同时活跃的线程数减少,可能降低延迟隐藏能力。这正是融合窗口策略需要权衡的核心矛盾——省了访存,但要付出并行度上的代价。
为何常见:初学者很难直观感受到寄存器资源的稀缺性,因为在日常编程中程序员很少直接管理寄存器,容易低估它的约束力。
面试题
Q1. 垂直融合最核心的优化目标是什么?
- A.减少算子的计算量
- B.消除中间结果的全局内存往返← 正确答案
- C.提升 CPU 主频
- D.增加并行线程数量
答案:B。垂直融合的本质是把相邻算子合并为单个 kernel,让中间结果驻留寄存器或共享内存。选项 B 正是指出这一核心目标。A 错在计算量并未减少,只是访存优化;C、D 与硬件机制无关,融合并不改变主频或线程数。
Q2. 在餐厅后厨的比喻中,“传菜员来回端盘子”对应 AI 计算中的什么操作?
- A.寄存器读写
- B.算子计算
- C.全局内存读写← 正确答案
- D.指令译码
答案:C。比喻中传菜员把菜从一位师傅端到另一位师傅,对应数据在算子间的传递。由于中间数据存放在全局内存中,传菜操作对应全局内存的读写(选项 C)。A 寄存器读写是融合后的理想状态,B 是师傅做菜的工序,D 与数据传递无关。
Q3. 融合窗口大小选择时,最重要的硬件约束是什么?
- A.缓存行大小
- B.寄存器文件容量← 正确答案
- C.指令缓存大小
- D.全局内存带宽
答案:B。融合窗口越大,中间结果同时驻留的寄存器越多。若超过寄存器文件容量,数据会溢出到本地内存,反而引入新开销。B 正确。缓存行、指令缓存和全局内存带宽虽然影响性能,但不是决定融合窗口大小的直接约束,融合决策主要围绕寄存器压力展开。
Q4. 数据流分析中,Fusable(A, B)=True 的条件是?
- A.A 和 B 在同层并行执行
- B.A 和 B 使用相同编程语言
- C.A 的输出与 B 的输入有交集← 正确答案
- D.A 和 B 的参数量相同
答案:C。垂直融合的前提是存在生产者-消费者依赖,即算子 A 的输出恰好被算子 B 作为输入消费,对应 C。A 是同层算子的水平融合条件;B 和 D 描述的是实现细节和参数规模,与数据依赖无关。
Q5. 先水平融合再垂直融合的优势是什么?
- A.水平融合可减少算子数量,简化后续垂直融合← 正确答案
- B.垂直融合可减少寄存器占用
- C.水平融合可提高计算精度
- D.垂直融合可降低编译时间
答案:A。先水平融合把同层多个算子合并,减少算子总数,之后垂直融合时待处理的算子序列更短、依赖关系更清晰,融合更高效。A 正确。B 说反了,垂直融合往往增加寄存器压力;C 和 D 与融合顺序无关。
Q6. 垂直融合后中间结果可以驻留在哪些存储位置?
- A.寄存器← 正确答案
- B.共享内存← 正确答案
- C.全局内存 HBM
- D.磁盘
答案:AB。垂直融合的目标是让中间结果尽量靠近计算单元,可驻留的位置包括寄存器(最快)和共享内存(次之),对应 A 和 B。C 全局内存正是融合要避免的慢速存储,D 磁盘是本外存储,与 GPU 计算无关。
Q7. 一个算子序列中,未融合时每个算子平均访存 120 周期,融合后整体访存降为 20 周期,若计算时间不变,访存占比为 80%,则融合加速比约为?
- A.1.2 倍
- B.2.0 倍
- C.4.0 倍← 正确答案
- D.6.0 倍
答案:C。设总时间 1,计算占 0.2,访存占 0.8。访存从 120 周期降到 20 周期,即访存时间降为原来的 1/6,访存部分变为 0.8/6=0.133。融合后总时间为 0.2+0.133=0.333,加速比 1/0.333≈3 倍。选项中 C 的 4.0 倍最接近。这道题考察访存占比和访存压缩比例的乘积关系。
Q8. 关于寄存器驻留的代价,下列说法正确的是?
- A.驻留数据越多,线程占用率越高
- B.驻留数据占寄存器会降低并行度← 正确答案
- C.寄存器容量无限,无需权衡
- D.驻留数据只影响共享内存
答案:B。寄存器是稀缺资源,中间结果占用越多,每个线程可用的寄存器越少,能同时驻留的线程数就越少,降低 occupancy(B 正确)。A 说反了,驻留越多占用率越低;C 明显违背硬件事实;D 混淆了寄存器和共享内存。
Q9. 垂直融合对以下哪些算子组合收益最明显?
- A.计算密集型且数据复用一个算子可能较大时← 正确答案
- B.卷积后紧跟元素级操作← 正确答案
- C.两个完全独立的并行分支
- D.输出被多个算子共享的中间层
答案:AB。垂直融合最适合数据流前后串联、中间结果只被消费一次的算子链,B 是典型的 Conv+BN+ReLU 融合场景,A 中数据复用大时融合收益显著。C 是与垂直融合无关的水平场景;D 中共享的中间结果即使融合也要多次输出,收益有限。
Q10. 某芯片寄存器总量 64KB,融合窗口内三个算子的中间结果分别需要 10KB、20KB、40KB 寄存器。若一次只能有一个算子驻留全部中间结果,下列哪个窗口策略是可行的?
- A.三个算子全部融合
- B.只融合前两个算子← 正确答案
- C.只能融合第一个算子
- D.不能融合任何算子
答案:B。融合前两个算子需要 10+20=30KB 寄存器,小于 64KB 可行。融合全部三个需要 70KB,超过 64KB 会溢出,因此 A 不可行。B 正确。C 虽然可行但不是最优,D 过于保守。此题的考点是融合窗口必须满足寄存器容量约束,这正对应“融合窗口策略”的精髓。