一、什么是算子:从数学函数到计算图中的基本单元

大家好,我是你们的老朋友。今天咱们聊聊算子融合,但得先从最基础的概念说起——算子到底是什么?

说实话,我刚入行那会儿,听到「算子」这个词也觉得挺唬人的。后来做了一年多的编译器开发,才慢慢摸清楚它的本质。说白了,算子就是计算图里的一个节点,它完成一个特定的数学运算。

1.1 从数学函数说起

你想想看,数学里的函数长什么样?

y = f(x)          // 一元函数
z = g(x, y)       // 二元函数
output = h(x1, x2, ..., xn)  // 多元函数

算子其实就是这个「f」、「g」、「h」。只不过在深度学习里,我们把它搬到了计算图上。

举个例子,一个简单的卷积算子:

# 伪代码示意
def conv2d(input, kernel):
    # input: [N, C, H, W]
    # kernel: [C_out, C_in, K_h, K_w]
    output = 对每个位置做乘加运算
    return output

嗯,这里要注意:算子接收输入张量,经过计算,输出新的张量。它就是一个计算单元。

1.2 计算图中的算子

我习惯把计算图想象成一个工厂流水线。每个算子就是一台机器,数据从一台机器流到另一台机器。

核心要点:算子 = 计算图中的节点 = 一个数学运算的封装

比如一个简单的神经网络层:

# 一个全连接层的计算图
输入 → MatMul(矩阵乘) → Add(偏置) → ReLU(激活) → 输出
        ↑算子1          ↑算子2       ↑算子3

这里 MatMul、Add、ReLU 都是算子。它们各自完成自己的工作,然后传给下一个。

1.3 算子的分类:计算密集型 vs 访存密集型

做算子融合的时候,这个分类特别重要。我踩过不少坑,都是因为没搞清楚算子的「性格」。

类型 特点 典型算子 瓶颈
计算密集型 计算时间远大于访存时间 卷积、矩阵乘、FFT 算力(FLOPS)
访存密集型 访存时间远大于计算时间 ReLU、Add、Reshape、Concat 带宽(GB/s)

计算密集型算子,说白了就是「算得多,读写得少」。比如一个 1024x1024 的矩阵乘,你要做几十亿次乘加,但输入输出数据也就几 MB。这时候 GPU 的算力是瓶颈。

访存密集型算子,就是「算得少,读写多」。比如 ReLU,每个元素就做个 max(0, x),但你要把整个张量从显存读进来,再写回去。这时候带宽是瓶颈。

我的经验:融合算子的时候,通常是把访存密集型的算子「粘」到计算密集型算子旁边。比如把 ReLU 融合进卷积,这样 ReLU 就不用单独读写一次显存了。

1.4 为什么这个分类这么重要?

我曾经优化一个模型,发现性能卡在了一堆 ElementWise 算子上。每个算子就做一点点计算,但都要读写一遍显存。这就是典型的访存瓶颈。

你想想看:

  • 一个卷积 + ReLU,分开执行:卷积写一次显存,ReLU 读一次再写一次
  • 融合后:卷积算完直接做 ReLU,省掉一次读写

对于大模型来说,省掉一次显存读写可能就是几十毫秒的差距。累积起来,整个训练时间能缩短 20%-30%。

避坑指南:我曾经把两个计算密集型算子(比如两个卷积)强行融合,结果因为寄存器压力太大,性能反而下降了。融合不是万能的,要看清算子的「性格」。

1.5 本章知识体系

为了让你更直观地理解,我画了一张图:

算子知识体系 算子定义 数学函数 → 计算图节点 计算图 流水线:算子1 → 算子2 → 算子3 算子分类 两类 计算密集型 计算时间 >> 访存时间 典型:卷积、矩阵乘、FFT 瓶颈:算力 (FLOPS) 访存密集型 访存时间 >> 计算时间 典型:ReLU、Add、Reshape 瓶颈:带宽 (GB/s) 融合策略 访存密集型 → 粘到 → 计算密集型旁边

这张图把整个知识脉络串起来了。从算子定义出发,到计算图,再到分类,最后落到融合策略。后面的章节我们会一步步深入每个环节。

1.6 小结

今天咱们聊了三个核心点:

  • 算子是什么:计算图里的基本运算单元,本质就是数学函数的封装
  • 计算图怎么组织算子:像流水线一样,数据依次流过每个算子
  • 算子分两类:计算密集型和访存密集型,它们的优化思路完全不同

我个人觉得,理解算子的分类是入门算子融合最关键的一步。你想想看,如果你连算子的「性格」都不了解,怎么知道该不该融合、怎么融合呢?

下一章咱们会深入计算图,看看算子之间是怎么连接的,以及为什么有些连接方式会导致性能问题。到时候我会拿一个真实项目里的例子来讲解,保证让你印象深刻。

可可精品资料库