在城市的快递分拣中心,每天早晨都堆着几百个包裹。如果每个包裹都单独派一名快递员跑一趟,光穿背心、扫码出库、查路线就要花掉好几分钟——车还没发动,半天就耗在了“准备出发”上。聪明的站长定了一条规矩:去同一个小区、顺路同类的包裹,装进同一辆车,一趟送完。省下的不是路上时间,而是每一趟出发前那笔固定的“启动开销”。 AI 编译器里的水平融合,做的就是同样的事。深度学习模型的计算图里有几十上百个算子(operator),每个算子都会被编译成一个 kernel(GPU 上执行的一段程序)。每启动一个 kernel,CPU 都要向 GPU 下发一次指令,这笔开销与 kernel 里干多少活无关。水平融合把“具有相同输入”或“独立执行的同类算子”合并成一个 kernel,让 GPU 一趟车跑完原本三趟的活,既减少了启动次数,又提升了并行度。

岗位关联

理解水平融合,是看懂编译器图优化的一把钥匙。掌握它,你就能读懂推理框架的优化日志,也能在实际部署中通过配置融合选项显著压低延迟。

目录
  1. 可视化演示
  2. 融合基本概念
  3. 水平匹配条件
  4. 合并调度策略
  5. 启动开销分析
  6. GPU性能收益
  7. 配置调优经验
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Transformer自注意力机制的QKV矩阵计算、注意力权重归一化及多头并行处理流程,步进切换查看注意力核心步骤。

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

QKV计算:线性变换生成查询键值 输入序列经线性变换生成Query Key Value三个矩阵 Q用于查询K用于匹配V提供输出内容 Q=X*Wq, K=X*Wk, V=X*Wv QKV是注意力计算的基础

融合基本概念

快递站里最常听见的声音是站长喊“出车!”。每喊一声,快递员就要从椅子上站起来、穿反光背心、扫码装车、查路线——这一整套动作,无论包裹是一个还是一百个,时间一分不少。在 GPU 计算的世界里,“出车”就是一次 kernel 启动,而快递员的固定准备时间,就是启动开销。

深度学习模型在训练或推理前,会被编译器转换成一幅计算图:节点是算子(比如加法、ReLU、矩阵乘),边是张量流动。编译器接下来要回答一个关键问题:这些算子怎么翻译成 GPU 上真正执行的 kernel?朴素的做法是一个算子一个 kernel,但小算子越多,启动开销就越刺眼。

于是有了算子融合。按合并对象之间的关系,融合分成两类。垂直融合合并的是“生产者—消费者”关系的算子,a 算完把结果交给 b,相当于接力赛——快递员送完一段,下一位在中转站接力;经典案例是把 Conv 和 ReLU 合成一个 kernel,省掉中间结果的显存往返。水平融合合并的则是“相同输入”或“独立执行的同类算子”,相当于拼单——三个包裹都去同一栋楼,装一辆车走一趟。

所以水平融合的本质,是快递站的那条规矩:拼单的前提是“顺路又同类”,而不是不管三七二十一把所有包裹硬塞一辆车——至于什么算顺路,正是下一节要讲的匹配条件。

Fhorizontal:{op1,op2,…,opk}→opfused,s.t.  input(opi)=input(opj) 或 type(opi)=type(opj)\displaystyle F_{\text{horizontal}}: \{ \text{op}_1, \text{op}_2, \ldots, \text{op}_k \} \to \text{op}_{\text{fused}}, \quad \text{s.t.} \ \ \text{input}(\text{op}_i) = \text{input}(\text{op}_j) \ \text{或} \ \text{type}(\text{op}_i) = \text{type}(\text{op}_j)
这个式子说的是:水平融合把 k 个算子打包成一个融合算子,前提是它们输入相同或类型相同。
  1. F_horizontal 变换水平融合把一组算子映射成一个融合算子,如同快递站把多个包裹归并到一辆车。
    FhorizontalF_{\text{horizontal}}
  2. op_1 至 op_k被融合的 k 个算子,是拼单前的 k 个独立包裹。
    {op1,op2,…,opk}\{ \text{op}_1, \text{op}_2, \ldots, \text{op}_k \}
  3. op_fused融合后生成的新算子,对应一次 kernel 启动,是那辆装满包裹的快递车。
    opfused\text{op}_{\text{fused}}
  4. s.t. 约束条件s.t. 是“使得/满足”之意,要求算子间输入相同或类型相同,作为拼单合法性的判据。
    input(opi)=input(opj) 或 type(opi)=type(opj)\text{input}(\text{op}_i) = \text{input}(\text{op}_j) \ \text{或} \ \text{type}(\text{op}_i) = \text{type}(\text{op}_j)

水平匹配条件

老站长在墙上贴了一张拼单须知:去不同小区的不能拼,生鲜和普通件不能混装,超过车厢容积的不能硬塞。水平融合同样有一张“拼单须知”,每条都对应一个编译期可检查的匹配条件。

编译器在图优化阶段会逐个扫描算子对,判断它们能否水平融合。判断的依据集中在四个维度。第一,相同输入依赖:两个算子读同一个张量(例如 add 和 multiply 都读取 tensor A),融合后这个张量只需读一次,能省访存带宽。第二,算子类型一致:一般要求算子属于同一类别(如都是 element-wise,即逐元素运算的算子),同类算子的计算模式接近,才能在同一个 kernel 里用同一种线程布局处理;类型差太远(一个矩阵乘、一个 ReLU),融合后的 kernel 会变得复杂且难以优化。第三,形状兼容:输入输出张量的形状和内存布局一致,否则线程与数据的映射关系无法统一。第四,无副作用约束:算子不能有全局副作用(比如更新全局计数器、写外部文件),否则合并后执行次序改变会破坏语义。

拼单须知上的每一条,都能在图结构里“对号入座”地检查。只有四张通行证全部盖章的算子对,才配被装进同一辆快递车。

fusable(opi,opj)  ⟺  (inputi=inputj ∨ typei=typej)∧shapei∼shapej∧¬ sideeffect\displaystyle \text{fusable}(\text{op}_i, \text{op}_j) \iff \big( \text{input}_i = \text{input}_j \ \lor \ \text{type}_i = \text{type}_j \big) \land \text{shape}_i \sim \text{shape}_j \land \neg \, \text{sideeffect}
可融合当且仅当:输入相同或类型相同,且形状兼容,且没有副作用。
  1. fusable 谓词判断两个算子能否进行水平融合的函数,编译器在图优化阶段逐对调用。
    fusable(opi,opj)\text{fusable}(\text{op}_i, \text{op}_j)
  2. 输入相同或类型相同两个条件满足其一即可,这是拼单的“顺路”依据。
    inputi=inputj ∨ typei=typej\text{input}_i = \text{input}_j \ \lor \ \text{type}_i = \text{type}_j
  3. 形状兼容记号 ~ 表示兼容,要求张量形状与内存布局一致,否则线程映射无法统一。
    shapei∼shapej\text{shape}_i \sim \text{shape}_j
  4. 无副作用约束取反号表示“副作用必须为假”,保证融合前后程序语义不变。
    ¬ sideeffect\neg \, \text{sideeffect}

合并调度策略

包裹装上车只是开始。到了小区,快递员是先送三号楼还是先送七号楼,走哪条路线能少爬楼梯,这才是真正决定一趟配送快慢的细节。水平融合后,新 kernel 内部同样要重新做一次“派送路线规划”。

融合完成不代表优化完成。合并后的 kernel 必须回答三个问题:谁来执行、执行多少、数据怎么读。典型的调度策略包含四点。单 kernel 统一调度:所有被融合的算子跑在同一次 kernel 启动里,由一个统一的 kernel 函数体处理,CPU 侧只发一次指令。线程块重划分:不再沿用单个算子的线程块配置,而是按合并后的总工作量重新计算。数据并行展开:把原本串行启动的独立计算展开到不同 SM(流式多处理器,GPU 的计算核心单元)上并行执行,这正是“提升并行度”的来源——一个算子可能填不满 GPU,k 个拼起来却能占满。访存复用优化:对相同输入只读取一次,把数据留在寄存器或共享内存里供多个算子复用。

一趟车出门,走哪条路、先爬哪栋楼,都是调度策略说了算。GPU 上那套线程块分配,就是快递员手里的派送地图——地图画得好,一车顶三车。

Nblocks=⌈k×nTblock⌉,n=元素总数, Tblock=每线程块线程数\displaystyle N_{\text{blocks}} = \left\lceil \frac{k \times n}{T_{\text{block}}} \right\rceil, \quad n = \text{元素总数}, \ T_{\text{block}} = \text{每线程块线程数}
合并后的线程块数量,由“算子数 × 每个算子的元素数”的工作总量除以每块线程数得到——活多了,车次就得多,但依然是“一趟出车”。
  1. N_blocks融合后 kernel 的线程块数量,决定 GPU 上的并行度。
    NblocksN_{\text{blocks}}
  2. k × n 总工作量k 个独立算子各处理 n 个元素,合并后可视为 k×n 份独立工作。
    k×nk \times n
  3. 向上取整符号⌈·⌉ 保证最后余下的元素也有线程承接,不会丢活。
    ⌈k×nTblock⌉\left\lceil \frac{k \times n}{T_{\text{block}}} \right\rceil
  4. T_block每个线程块内的线程数,取 128 或 256 的倍数通常效率最高。
    TblockT_{\text{block}}

启动开销分析

快递老手的账本上,一趟配送的时间分成两块:出发前的准备时间和路上的派送时间。准备时间再短,它也是固定的——不管你这一趟装一个包裹还是十个。这就是“固定开销”最扎心的地方:它是叠加的。

GPU 编程里,CPU 每次启动一个 kernel,都要经过“准备参数→下发指令→GPU 队列排队→执行”的流程。即使现代框架做了异步提交,每一次启动的 CPU 侧指令开销仍然真实存在,通常在几十微秒量级。可别小看这几十微秒——当一个 element-wise 算子本身只算 10 微秒时,启动开销是计算时间的五倍。

关键在对比。假设融合前 k 个算子各自启动,总时间是 k 份“启动开销 + 计算时间”;水平融合后只启动一次,计算仍然要做(每个元素该算的逃不掉),但启动开销只剩一份。于是省下来的时间是 (k-1) 倍的启动开销。这个模型解释了为什么算子越小、融合收益越夸张。

账算明白了,站长就懂了:拼单拼的不是热闹,是“少付几遍出车费”。启动开销占比越高的活,越值得水平融合——这也是下一节要验证的 GPU 收益。

T前置=k(L+E)⟶T融合=L+kE,ΔT=(k−1)L\displaystyle T_{\text{前置}} = k (L + E) \quad \longrightarrow \quad T_{\text{融合}} = L + kE, \qquad \Delta T = (k-1) L
这是全篇最核心的一行推导:融合省下的时间等于“算子数减一”乘以启动开销——省的是出车费,不是路费。
  1. L 启动开销单次 kernel 启动开销,像快递员每次出车的固定准备时间,与装货量无关。
    LL
  2. E 计算时间单个算子的计算时间,像路上真实的派送时间。
    EE
  3. k(L+E)融合前总时间,k 次启动付出 k 份启动开销,括号展开为 kL + kE。
    k(L+E)k (L + E)
  4. L + kE融合后总时间,只有一次启动,计算时间保持 kE 不变。
    L+kEL + kE
  5. ΔT 节省量两者相减,启动开销部分从 kL 缩成 L,净省 (k-1)L。
    (k−1)L(k-1) L

GPU性能收益

拼单试运行一个月后,站长拉出配送报表:车次少了 60%,每趟车满载率上去了,小区门口排队的电瓶车也少了。GPU 的 performance report 同样会告诉你:启动次数、SM 占用率、访存带宽——每一项都在讲同一个故事。

水平融合的收益在 GPU 上分为三个层次,对应三种资源:时间、计算单元、带宽。一是延迟收益:启动次数从 k 降到 1,CPU 侧下发指令的密集程度大幅下降,GPU 队列的空洞变少,端到端单次推理延迟降低。二是并行度收益:独立算子合并后,总工作量 k×n 可以展开到更多线程块,把原本闲置的 SM 填满;单个小算子常常只用到 GPU 的一小块算力,拼起来才能“满载发车”。三是访存收益:相同输入只读一次,省下的显存带宽在小算子场景下非常可观。

实际数据上,在包含大量 element-wise 算子的 Transformer 推理图中,水平融合通常能带来 10%~30% 的端到端加速;小算子密集的老模型上收益更明显。一个典型场景:某模型每层末尾有 4 个逐元素算子,单独启动每次计算约 8 微秒、启动开销约 50 微秒,融合后单层耗时从 232 微秒降到 82 微秒,接近三倍提速。

GPU 报表上的红箭头一翘,站长的拼单方案才算真正落地。但报表也提醒:不是所有算子都适合拼单——这就要说到最后的调优经验了。

S=T前置T融合=k(L+E)L+kE\displaystyle S = \frac{T_{\text{前置}}}{T_{\text{融合}}} = \frac{k (L + E)}{L + k E}
加速比是融合后性能提升的量化口径:k 越大、L 相对 E 越突出,S 越接近 k 倍。
  1. S 加速比融合前后总耗时之比,大于 1 表示融合更快。
    SS
  2. 分子 k(L+E)融合前总耗时,k 份启动开销叠加。
    k(L+E)k (L + E)
  3. 分母 L+kE融合后总耗时,只剩一份启动开销。
    L+kEL + k E
  4. 极限分析当 L 远大于 E 时,S 趋近于 k,说明小算子场景收益最大。
    L≫E⇒S→kL \gg E \Rightarrow S \to k

配置调优经验

拼单上瘾后,站长把鸡蛋、沙发、汽油桶都塞进一辆车——结果生鲜坏了、沙发蹭花了、小区还进了消防队。拼单有边界。优秀的站长会建立一套“拼单策略”:先看数据,再定规则,最后小范围试跑。

编译器同样内置了融合的“拼单策略”,用户可以在部署时通过配置项影响它。开源框架(如 TVM、TensorRT、ONNX Runtime)普遍支持融合开关与融合等级。调优经验可以总结为四步。打开融合开关:将编译优化等级调到 O2/O3,或显式开启 fusion 选项。依靠启发式规则:编译器默认做一轮启发式判断——启动开销占比高于阈值、算子规模小、类型匹配的算子对优先触发融合。用性能剖析验证:跑一遍 profiler(如 nsys、nvprof),看 kernel 耗时分布和启动次数,定位哪些小 kernel 值得融合。权衡副作用:留意融合后的寄存器压力与占用率变化,融合过度可能触发寄存器溢出(register spill,寄存器不够用被迫把数据存回显存),反而拖慢执行。实践中“小算子优先、大算子谨慎”是普遍心法。

站长的拼单策略最终变成墙上的一张流程图:看数据 → 定规则 → 试跑 → 复盘。水平融合的调优没有银弹,只有“剖析驱动”这一条踏实路——它把玄学变成了工程。

Occupancy=Nactive warpsNmax warps per SM\displaystyle \text{Occupancy} = \frac{N_{\text{active warps}}}{N_{\text{max warps per SM}}}
占用率是融合调优的仪表盘:融合后若占用率明显下降,说明线程块配置或寄存器用量出了问题,需要回退或调整。
  1. Occupancy 占用率SM 上活跃 warp(一组 32 个线程的调度单位)数与硬件上限的比值。
    Occupancy\text{Occupancy}
  2. N_active warps当前 kernel 实际可并行的 warp 数。
    Nactive warpsN_{\text{active warps}}
  3. N_max warps per SM硬件允许的最大 warp 数,由 GPU 架构决定。
    Nmax warps per SMN_{\text{max warps per SM}}
  4. 调优平衡点占用率太低说明 GPU 没吃饱,太高可能伴随寄存器压力,要取平衡。
    Nactive warpsNmax warps per SM\frac{N_{\text{active warps}}}{N_{\text{max warps per SM}}}

AI 应用场景

推理引擎的图优化阶段

阶段:模型编译与图优化

解决的问题:部署阶段的推理图里挤满了逐元素的小算子(激活、残差加法、缩放),每个单独 kernel 的计算只有几微秒到几十微秒,而启动开销往往比计算还贵。传统做法是原样执行每个算子,CPU 频繁下发指令,GPU 经常空转等待。水平融合把顺路的同类算子合并成一次启动,让 CPU 少喊话、GPU 多干活,直接压低延迟。

具体案例:某团队在 T4 GPU 上部署 BERT 推理服务,每个 Transformer 层末尾有 3 个逐元素算子。用推理引擎的图优化开启水平融合后,这三个算子合并为一个 kernel,端到端 P99 延迟下降约 18%,吞吐提升 22%。

训练框架的算子合并

阶段:训练图编译(如 PyTorch 的图模式编译)

解决的问题:训练时前向与反向计算图同样包含大量逐元素算子,单独启动在数据并行规模大时浪费宝贵的 GPU 算力。水平融合让多个独立算子并发展开到更多线程块,提高 SM 占用率,同时把相同输入只读一次,缓解显存带宽压力。

具体案例:用 torch.compile 编译一个视觉模型,自动融合了 backbone 中多个独立的 ReLU 与加法算子,训练吞吐提升 8%~15%,显存峰值降低约 5%。

Python 演示

python31 行
"""水平融合性能模拟:对比分离启动与融合启动的耗时"""  class FusionSimulator:    def __init__(self, launch_us=50, compute_us=30, parallel_gain=2.0):        self.launch_us = launch_us          # 单次 kernel 启动开销(微秒)        self.compute_us = compute_us        # 单个算子计算耗时(微秒)        self.parallel_gain = parallel_gain  # 融合后并行度提升系数     def run_separate(self, op_count):        """融合前:k 个算子分别启动,每个都要付一份启动开销"""        return op_count * (self.launch_us + self.compute_us)     def run_fused(self, op_count):        """水平融合:只启动一次,计算量因并行展开而摊薄"""        return self.launch_us + self.compute_us * op_count / self.parallel_gain     def report(self, op_count):        t0 = self.run_separate(op_count)        t1 = self.run_fused(op_count)        print(f"算子数={op_count:2d} 分离={t0:6.1f}us 融合={t1:6.1f}us 加速比={t0 / t1:.2f}x")  def main():    sim = FusionSimulator(launch_us=50, compute_us=30)    for n in (2, 4, 8, 16):        sim.report(n)  if __name__ == "__main__":    main()
目的:模拟不同算子数量下“分离启动”与“水平融合”两种模式的总耗时,直观展示融合收益随算子数增长的规律。
  1. self.launch_us = launch_us记录单次 kernel 启动的固定开销,相当于快递员每次出车的准备时间。
  2. def run_separate(self, op_count)模拟融合前:k 个算子各自启动,每次都支付一份启动开销。
  3. return op_count * (self.launch_us + self.compute_us)对应公式 T= k(L+E),k 次启动叠加。
  4. def run_fused(self, op_count)模拟水平融合:所有算子合并成一个 kernel,只启动一次。
  5. self.parallel_gain代表独立算子展开到更多 SM 带来的并行收益,是简化模型的核心假设。
  6. print(f"...")输出各算子数下的对比,加速比随 k 增大而上升。
  7. if __name__ == "__main__":可执行入口,保证模块既可导入也可直接运行。

常见误区

误区既然融合能省启动开销,那就把计算图里所有算子都合并成一个超大的 kernel,一步到位,肯定最快。

正解水平融合只合并“满足匹配条件”的算子:相同输入或独立同类、形状兼容、无副作用。乱拼单就像把生鲜和家具塞进同一辆车,绕路、超载反而更慢。融合过度会导致寄存器压力上升、占用率下降,甚至触发寄存器溢出(register spill),性能不升反降。编译器会做启发式判断,而不是无脑全融。

为何常见:把“算子融合”理解成单一操作,忽略了匹配条件和硬件资源约束,只看到启动开销这一个变量。

误区水平融合把好几个算子合并了,数学运算量肯定也变少了吧?模型应该会“算得快一倍”。

正解水平融合主要省的是 kernel 启动开销和部分额外访存,算子本身的数学计算量基本不变——每个元素该做的加法、激活一个都逃不掉。收益来自“少付 (k-1) 份出车费”和并行度提升,不是计算被约掉了。真正能省中间存储操作的,是垂直融合(如 Conv+ReLU 避免中间张量写回显存),两者收益来源不同。

为何常见:把水平融合和垂直融合的收益混为一谈,误以为“融合”就是“数学上合并同类项”。

误区编译器说这两个算子能融合,那就放心融合吧,反正不会比原来差。

正解可融合只代表“语义上安全”,不代表“性能上划算”。小 kernel 融合收益大;但本来就把 GPU 占满的大算子,合并后可能因调度灵活性降低、寄存器溢出而变慢。就像逢车必拼,有时等拼单的时间比单独发车还久。正确做法是融合后用 profiler 验证,用实测数据说话。

为何常见:忽略了融合后的线程块重划分、寄存器分配等二次开销,把“可融合”等同于“该融合”。

误区都是把算子合并,名字里都有“融合”,区别不大,随便记一个就行。

正解垂直融合(vertical fusion)合并的是有生产者—消费者关系的算子(a 算完,b 用 a 的结果),像接力赛,省的是中间张量的显存往返;水平融合合并的是相同输入或独立的同类算子,像拼单,省的是启动开销、提升并行度。两者依赖关系不同、匹配条件不同、收益模型也不同。

为何常见:只看到“融合”两个字的表面相似,没有深入分析算子之间的数据依赖关系。

面试题

Q1. 水平融合主要针对哪类算子进行合并?

单选题 · easy

  • A.具有相同输入或独立同类算子← 正确答案
  • B.存在生产者消费者级联算子
  • C.包含复杂循环控制流态算子
  • D.输入输出形状完全不同的算子

答案:A。水平融合合并相同输入或独立执行的同类算子,如同把顺路包裹拼一辆车;生产者—消费者关系由垂直融合处理,复杂控制流和形状不兼容的算子不满足融合条件。

Q2. 水平融合最主要的直接收益是什么?

单选题 · easy

  • A.大量减少内核启动次数← 正确答案
  • B.显著缩小模型的参数量
  • C.直接降低训练数据的维度
  • D.自动提升卷积核的尺寸

答案:A。水平融合把 k 次 kernel 启动合并为 1 次,直接收益是减少启动开销。它不改变模型参数量、不改变数据维度,也与卷积核尺寸无关,那些是模型结构层面的问题。

Q3. 以下哪项属于水平融合的匹配条件?

单选题 · mid

  • A.算子类型一致且输入张量相同← 正确答案
  • B.算子必须分属不同计算设备
  • C.算子输出必须是单个标量值
  • D.算子只能出现在循环体内部

答案:A。匹配条件包括相同输入依赖、算子类型一致、形状兼容、无副作用。分属不同设备、输出标量、必须出现在循环内都不属于水平融合的要求,甚至与融合目标相悖。

Q4. 水平融合后单个算子的数学计算量通常会怎样?

单选题 · mid

  • A.基本不变省的是启动开销← 正确答案
  • B.自动减少到原来的约一半
  • C.翻倍增加以保证计算正确
  • D.完全消失并合并成单个标量

答案:A。水平融合不改变每个算子本身的数学计算,该算的元素一个不少,省的是启动开销和部分多余访存。把计算量“减少一半”或“翻倍”都没有理论依据,是混淆了垂直融合的收益模型。

Q5. kernel 启动开销在 GPU 上主要来自哪里?

单选题 · mid

  • A.CPU 向 GPU 下发指令的交互开销← 正确答案
  • B.GPU 内部寄存器文件读写等待
  • C.显存带宽不足引起的排队拥塞
  • D.模型参数量过大导致的页面换入

答案:A。kernel 启动指 CPU 准备参数并向下发指令给 GPU 的过程,开销集中在 CPU—GPU 交互链路。寄存器读写等待、显存带宽拥塞都发生在 kernel 执行阶段,与启动动作无关。

Q6. 设单次启动开销为 L,单算子计算为 E,k 个算子水平融合后的加速比是?

单选题 · hard

  • A.k(L+E)/(L+kE)← 正确答案
  • B.(L+kE)/(k(L+E))
  • C.(k-1)L/(kE)
  • D.kL/(L+kE)

答案:A。融合前总耗时 k(L+E),融合后总耗时 L+kE,加速比是两者之比,即 A。B 是倒数,C 少了计算项,D 少了融合后的计算时间,都不符合启动开销模型。

Q7. 水平融合后的 kernel 应如何重新分配线程块?

单选题 · hard

  • A.按合并后总工作量重新计算← 正确答案
  • B.沿用第一个算子的线程块配置
  • C.固定只使用一个线程块执行
  • D.由用户逐核手工指定线程数

答案:A。融合后总工作量为 k×n,需要按新的总量重新划分线程块才能占满 SM。沿用旧配置会低估工作量,固定单块会浪费并行能力,手工逐核指定既不现实也不可移植。

Q8. 水平融合需要满足哪些匹配条件?

多选题 · mid

  • A.算子拥有相同输入或独立执行← 正确答案
  • B.算子类型一致或完全相互兼容← 正确答案
  • C.张量形状和数据布局相互兼容← 正确答案
  • D.算子必须输出相同的变量名称

答案:ABC。水平融合要求相同输入或独立执行、类型一致或兼容、形状布局兼容,且无副作用。变量名称是编译期符号,与融合正确性无关,属于干扰项。

Q9. 水平融合能带来哪些性能收益?

多选题 · mid

  • A.减少内核启动带来的固定开销← 正确答案
  • B.提升 GPU 并行度和资源利用率← 正确答案
  • C.相同输入只读一次节省访存带宽← 正确答案
  • D.减少模型训练所需的迭代轮数

答案:ABC。水平融合在时间、计算单元、带宽三个层面受益:省启动开销、填满 SM 提升并行度、复用输入省访存。训练轮数由优化算法和数据决定,与算子融合无关。

Q10. 关于水平融合的实战调优经验正确的是?

多选题 · hard

  • A.先做性能剖析再决定是否融合← 正确答案
  • B.关注融合后寄存器压力变化← 正确答案
  • C.所有同类算子必须无条件融合
  • D.用启发式规则控制融合的触发← 正确答案

答案:ABD。实战经验是剖析驱动:先看 profiler 定位小 kernel,再让启发式规则决定是否融合,并持续关注寄存器压力与占用率变化。无条件融合忽略了硬件资源约束,融合过度反而可能变慢。