2. 融合策略全景图:从计算图到硬件映射的桥梁

大家好,我是你们的老朋友。今天我们来聊聊融合策略的全景图。

说实话,很多同学一上来就问我:「到底该用哪种融合?」「算子融合是不是越多越好?」

嗯,这些问题其实都指向同一个核心——你需要在计算图和硬件之间,搭一座桥。这座桥怎么搭,决定了你的模型跑得快不快,内存带宽够不够用。

2.1 为什么需要这张全景图?

我刚开始做优化的时候,也踩过坑。有一次,我把一个模型里能融合的算子全合在一起了,结果性能反而下降了。后来一查,原来是融合后的中间结果太大,L1缓存根本放不下。

你想想看,融合策略不是简单的「合并同类项」。它要回答三个问题:

  • 什么时候该融合?—— 内存带宽吃紧的时候
  • 融合到什么程度?—— 是两两融合,还是整块融合?
  • 融合后怎么映射?—— 硬件上怎么调度?

说白了,这张全景图就是帮你做决策的「导航仪」。

2.2 计算图视角:融合的「原材料」

我们先从计算图说起。计算图就是模型的「骨架」,每个节点是一个算子,每条边是数据流动。

我个人习惯把算子分成三类:

算子类型 典型代表 特点
计算密集型 Conv、MatMul 算力瓶颈,内存访问少
访存密集型 ReLU、Add、Reshape 内存带宽瓶颈,计算量小
数据依赖型 Gather、Scatter 不规则访问,难以融合

为什么要分这个?因为融合策略主要针对访存密集型算子。你想想看,如果两个计算密集型算子硬凑在一起,反而会打乱缓存局部性。

核心原则:融合的目标是减少「中间结果写回内存」的次数。说白了,就是把数据留在寄存器或缓存里,别来回倒腾。

2.3 硬件映射视角:融合的「施工图」

计算图画好了,接下来就是怎么映射到硬件上。这里我画了一张图,帮你理解整个流程:

计算图层 Conv ReLU Add BN ReLU 融合策略层 水平融合 垂直融合 跨块融合 硬件映射层 寄存器分配 缓存优化 内存调度 指令流水线

这张图展示了从计算图到硬件映射的完整链路。我习惯把它叫做「三层架构」:

  • 计算图层:原始模型结构,算子之间通过边连接
  • 融合策略层:决定哪些算子合并,怎么合并
  • 硬件映射层:把融合后的算子块,映射到具体的硬件资源上

我的经验:很多同学只关注中间那层,忽略了上下两层。其实,计算图的结构决定了融合的上限,硬件的特性决定了融合的下限。你想想看,如果计算图本身就有大量分支,再好的融合策略也救不了。

2.4 三种核心融合模式

在实际项目中,我总结出三种最常用的融合模式。每种模式都有它的「脾气」:

2.4.1 水平融合

把同一层级的、输入相同的算子合并。比如两个独立的卷积层,输入都是同一个特征图。

  • 优点:减少重复读取输入数据
  • 缺点:融合后计算量可能暴增
  • 适用场景:Inception 类网络、多分支结构

2.4.2 垂直融合

把前后串联的算子合并。比如 Conv + ReLU + BN 合并成一个算子。

  • 优点:消除中间结果的写回
  • 缺点:对硬件寄存器数量要求高
  • 适用场景:ResNet、VGG 等直筒型网络

2.4.3 跨块融合

跨越多个计算块进行融合。比如把两个残差块的部分算子合并。

  • 优点:进一步减少全局内存访问
  • 缺点:调度复杂度指数级上升
  • 适用场景:大模型、长序列任务

避坑指南:我曾经在一个 Transformer 模型上尝试跨块融合,结果因为中间变量太多,寄存器溢出,性能反而下降了 30%。所以,跨块融合一定要先做内存预算,算清楚你的硬件到底能装下多少中间结果。

2.5 融合策略的决策流程

好了,理论讲完了,我们来点实战的。我一般按照这个流程来做融合决策:

  1. 分析计算图:找出所有访存密集型算子对
  2. 评估内存带宽:看看当前瓶颈在哪里
  3. 选择融合模式:根据算子关系选水平/垂直/跨块
  4. 模拟硬件映射:估算寄存器、缓存使用量
  5. 性能验证:跑 benchmark,看实际效果

这里我贴一段伪代码,帮你理解这个流程:

def fusion_decision(graph, hardware_info):
    # 1. 分析计算图
    ops = analyze_ops(graph)
    
    # 2. 评估内存带宽
    bandwidth_bottleneck = find_bottleneck(ops, hardware_info)
    
    # 3. 选择融合模式
    if bandwidth_bottleneck > threshold:
        fusion_pairs = find_vertical_pairs(ops)  # 优先垂直融合
    else:
        fusion_pairs = find_horizontal_pairs(ops)  # 水平融合
    
    # 4. 模拟硬件映射
    for pair in fusion_pairs:
        reg_usage = estimate_register_usage(pair)
        if reg_usage > hardware_info.reg_count:
            fusion_pairs.remove(pair)  # 寄存器不够,放弃融合
    
    return fusion_pairs

这段代码虽然简单,但核心逻辑都在里面了。我个人习惯把「寄存器预算」作为第一道门槛——硬件装不下,再好的融合也是白搭。

2.6 小结

嗯,今天的内容就到这里。我们聊了融合策略的全景图,从计算图到硬件映射,再到三种核心融合模式。

你可能会问:「这么多模式,我到底该用哪个?」

我的建议是:从垂直融合开始。它最直观,效果也最稳定。等你把垂直融合玩熟了,再考虑水平融合和跨块融合。

记住一句话:融合不是目的,减少内存带宽压力才是。别为了融合而融合,那只会让你的代码更复杂,性能更差。

想要更多资料,联系可可