01 · 数学很简单,执行却要来回搬数据
看一段小计算:先把两个同形状向量相加,A=X+B;再把负数变为0,Y=ReLU(A)。数学上,每个位置只需要一次加法和一次比较。但分开执行时,第一个设备内核写出A,第二个内核重新读取A。
算子是框架中的逻辑操作,例如加法、ReLU、矩阵乘法;kernel是实际提交给设备的一段执行程序。一个逻辑算子可以启动多个kernel,多个逻辑算子也可以被合进一个kernel。先把这两个概念分开,就容易理解融合。
如果计算本身很少,中间数据读写和重复启动可能占了主要成本。融合尝试让相邻操作在同一段执行中衔接,把本来要写回再读入的临时结果留在寄存器或片上存储中。
02 · 用四个数走完未融合流程
X=[-3, 1, 2, -1]
B=[ 1, 2,-4, 2]
内核1:读取X和B → 相加 → 写出A=[-2,3,-2,1]
内核2:读取A → ReLU → 写出Y=[0,3,0,1]
融合:读取X和B → 每个位置相加后立即ReLU → 写出Y
位置1:1+2=3,max(3,0)=3融合没有跳过加法或ReLU,也没有改变它们的依赖。改变的是执行边界和临时值的存放:位置1的和算出来后,紧接着比较,最后只保存3。
03 · 到底省掉多少读写
为了便于计数,假设X、B、A、Y都含N个元素,每元素b字节;B是同形状输入,不是常见的通道广播偏置。逐元素分离基线逻辑上读X、B并写A,再读A并写Y,共5Nb字节;融合读X、B并写Y,共3Nb字节。
| 执行 | 逻辑读写 | 启动边界 |
|---|---|---|
| 分开相加与ReLU | 读X、B;写A;读A;写Y = 5Nb | 两个内核 |
| 融合,不需保存A | 读X、B;写Y = 3Nb | 示例中一个内核 |
| 融合,但其他消费者需要A | 读X、B;写A、Y = 4Nb | 示例中仍可一个内核 |
相加+ReLU:省掉的是哪次读写
X、B为同形状输入,按完整物化张量计逻辑字节;不含缓存、广播、工作区和训练保存。字节减少比例不是实测加速比。04 · 融合内核如何逐块完成这些工作
for each assigned tile:
x = load(X[tile])
b = load(B[tile])
a = x + b # 暂存在寄存器等片上资源
y = maximum(a, 0)
store(Y[tile], y)
# 尾部不足一块时,用边界mask避免越界每个位置的计算独立,适合按连续小块分配。实际内核还要选择并行粒度、向量化、对齐和边界处理。这里并没有承诺所有值都进入共享内存:寄存器、共享内存、缓存和昇腾片上缓冲各有容量与用途。
有时还可把偏置、缩放或激活接在矩阵乘法的输出阶段,称为epilogue,即主计算后的收尾。这需要矩阵内核或库支持相应语义;不能用Python把表达式写在一行就认为已经融合。
依据:PyTorch 2.14 · torch.compile参数、模式与后端 ↗ · Triton · Fused Softmax官方教程 ↗
05 · 遇到归约,融合为何更难
相加和ReLU只看当前位置;Softmax、LayerNorm、RMSNorm则需要一行或一个范围的统计量。例如Softmax要先获得行最大值和指数和,再给每个元素归一化。融合要协调这类跨元素依赖。
Triton的融合Softmax教程以一行能够放入片上资源的矩阵为主要例子:读入一行,在片上做统计和归一化,再写结果。行太长时,需要别的分块或多阶段方案,不能把“全部操作塞进一个内核”当作普遍最优解。
FlashAttention也减少大型中间结果的物化,但它还有在线Softmax、分块Attention和训练重算等设计;“算子融合”可以描述其一部分机制,不能替代整篇算法解释。
06 · 前向省下A,训练反向还怎么算
上例在A不为0的位置,ReLU反向将dY传给正值位置,负值位置梯度为0。若dY=[1,2,3,4],则dX=dB=[0,2,0,4]。反向仍需要判断对应位置是否激活。
在这个简单例子中,可以从Y>0得到掩码,因此未必需要完整保存A。更复杂的激活函数可能需要输入、中间统计或重计算;融合前向不自动等于不保存激活,也不保证反向同样融合。
07 · 为什么有些相邻操作不值得融合
- A有其他消费者:为了它们仍需保存A,收益会改变。
- 融合扩大寄存器或片上缓冲占用:可能降低同时驻留任务数,甚至溢出到外部存储。
- 操作需要全局同步或不同最佳并行布局:强行融合可能比各用专用内核更慢。
- 广播、非连续布局、原地修改与别名:必须维持正确索引及读写顺序。
- 基线已有优化:应对比框架实际发出的内核,而非假设每个Python操作都单独执行。
08 · 与图编译、图重放和MC²有什么不同
| 方法 | 改变哪里 | 没有自动做到什么 |
|---|---|---|
| 本地融合 | 同设备相邻计算的数据流与执行边界 | 不自动消除跨卡通信。 |
| 图编译 M018 | 分析计算图并选择融合、布局、内核等 | 编译成一张图不保证只有一个kernel。 |
| 执行图重放 M019 | 复用已记录的任务与依赖,减少下发 | 重放可以保留原来的多个kernel和中间张量。 |
| 通算融合 M027 | 把通信和计算一起分块、组织进度 | 不能按本地加法+ReLU的读写模型理解。 |
这些方法可以组合。例如先编译得到融合内核,再把多个内核及依赖捕获下来重放。每一层收益要分别测量,不能把几次局部加速比直接相乘。
09 · 从正确的示例走向真实实现
下方CPU脚本比较分离和融合的输出、逻辑读写计数与反向梯度,并用有限差分校验远离ReLU零点的输入梯度。它使用Python循环,不代表设备内核,也不测速度。
def pointwise(x, b):
return torch.relu(x + b)
optimized = torch.compile(pointwise)
# 这是编译候选,不是“已融合”的证据。
# 用Profiler或生成代码核对真实kernel与中间分配。NVIDIA环境可从Inductor或Triton融合教程学习;昇腾图编译可从TorchAir入口核对后端支持。两家都要检查实际算子、dtype、shape和布局,不能把CUDA/Triton内核代码直接当作NPU实现。
依据:PyTorch · Performance Tuning Guide ↗ · Ascend · TorchAir官方仓库与NPU后端入口 ↗
10 · 哪些指标能证明优化有效
- 固定输入形状、dtype和质量要求,核对数值及训练梯度。
- 记录实际kernel数量、临时张量峰值与内存流量;逻辑计数与实测分开。
- 同步后测设备时间,也测包含调度的端到端时间。
- 观察长行、尾块、非连续输入与其他消费者,避免只测最有利形状。
- 把局部收益放回模型流程:该区域若只占总时间5%,其提速对整步仍有限。
自查:为什么融合仍做同样数学运算却可能更快?A必须给另一分支使用时还能省哪些读写?为什么前向不保存A,并不意味着所有激活函数都能照搬这个办法?
流程位置与关联方法
这个方法可能出现在多个位置;计算线包含模型计算与算子实现,执行线包含运行时、内存和通信。
资料与核查范围
原理依据以原始论文和官方文档为主;算例与时间线是教学推导。文档核查日期:2026-10-10。引用 main / latest 文档时,实际接口以安装版本为准。
- PyTorch · Performance Tuning Guide ↗
- Triton · Fused Softmax官方教程 ↗
- PyTorch 2.14 · torch.compile参数、模式与后端 ↗
- Ascend · TorchAir官方仓库与NPU后端入口 ↗
- FlashAttention 原始论文 ↗
代码为接入片段或教学伪代码;本页没有声称完成 NVIDIA / 昇腾硬件实测。