2. 融合策略全景图:从计算图到硬件映射的桥梁
大家好,我是你们的老朋友。今天我们来聊聊融合策略的全景图。
说实话,很多同学一上来就问我:「到底该用哪种融合?」「算子融合是不是越多越好?」
嗯,这些问题其实都指向同一个核心——你需要在计算图和硬件之间,搭一座桥。这座桥怎么搭,决定了你的模型跑得快不快,内存带宽够不够用。
2.1 为什么需要这张全景图?
我刚开始做优化的时候,也踩过坑。有一次,我把一个模型里能融合的算子全合在一起了,结果性能反而下降了。后来一查,原来是融合后的中间结果太大,L1缓存根本放不下。
你想想看,融合策略不是简单的「合并同类项」。它要回答三个问题:
- 什么时候该融合?—— 内存带宽吃紧的时候
- 融合到什么程度?—— 是两两融合,还是整块融合?
- 融合后怎么映射?—— 硬件上怎么调度?
说白了,这张全景图就是帮你做决策的「导航仪」。
2.2 计算图视角:融合的「原材料」
我们先从计算图说起。计算图就是模型的「骨架」,每个节点是一个算子,每条边是数据流动。
我个人习惯把算子分成三类:
| 算子类型 | 典型代表 | 特点 |
|---|---|---|
| 计算密集型 | Conv、MatMul | 算力瓶颈,内存访问少 |
| 访存密集型 | ReLU、Add、Reshape | 内存带宽瓶颈,计算量小 |
| 数据依赖型 | Gather、Scatter | 不规则访问,难以融合 |
为什么要分这个?因为融合策略主要针对访存密集型算子。你想想看,如果两个计算密集型算子硬凑在一起,反而会打乱缓存局部性。
核心原则:融合的目标是减少「中间结果写回内存」的次数。说白了,就是把数据留在寄存器或缓存里,别来回倒腾。
2.3 硬件映射视角:融合的「施工图」
计算图画好了,接下来就是怎么映射到硬件上。这里我画了一张图,帮你理解整个流程:
这张图展示了从计算图到硬件映射的完整链路。我习惯把它叫做「三层架构」:
- 计算图层:原始模型结构,算子之间通过边连接
- 融合策略层:决定哪些算子合并,怎么合并
- 硬件映射层:把融合后的算子块,映射到具体的硬件资源上
我的经验:很多同学只关注中间那层,忽略了上下两层。其实,计算图的结构决定了融合的上限,硬件的特性决定了融合的下限。你想想看,如果计算图本身就有大量分支,再好的融合策略也救不了。
2.4 三种核心融合模式
在实际项目中,我总结出三种最常用的融合模式。每种模式都有它的「脾气」:
2.4.1 水平融合
把同一层级的、输入相同的算子合并。比如两个独立的卷积层,输入都是同一个特征图。
- 优点:减少重复读取输入数据
- 缺点:融合后计算量可能暴增
- 适用场景:Inception 类网络、多分支结构
2.4.2 垂直融合
把前后串联的算子合并。比如 Conv + ReLU + BN 合并成一个算子。
- 优点:消除中间结果的写回
- 缺点:对硬件寄存器数量要求高
- 适用场景:ResNet、VGG 等直筒型网络
2.4.3 跨块融合
跨越多个计算块进行融合。比如把两个残差块的部分算子合并。
- 优点:进一步减少全局内存访问
- 缺点:调度复杂度指数级上升
- 适用场景:大模型、长序列任务
避坑指南:我曾经在一个 Transformer 模型上尝试跨块融合,结果因为中间变量太多,寄存器溢出,性能反而下降了 30%。所以,跨块融合一定要先做内存预算,算清楚你的硬件到底能装下多少中间结果。
2.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 小结
嗯,今天的内容就到这里。我们聊了融合策略的全景图,从计算图到硬件映射,再到三种核心融合模式。
你可能会问:「这么多模式,我到底该用哪个?」
我的建议是:从垂直融合开始。它最直观,效果也最稳定。等你把垂直融合玩熟了,再考虑水平融合和跨块融合。
记住一句话:融合不是目的,减少内存带宽压力才是。别为了融合而融合,那只会让你的代码更复杂,性能更差。
想要更多资料,联系可可