算子优化基础:从概念到实战
大家好,我是你们这堂课的老朋友。今天咱们聊聊算子优化的基本功。说实话,这行干久了,你会发现很多花里胡哨的技巧,底层都离不开这几个核心概念。
什么是算子?
先说说算子。很多人一听到「算子」就觉得高大上,其实没那么玄乎。
算子,说白了就是神经网络中的一个计算单元。 比如卷积、矩阵乘法、激活函数、池化……这些都是算子。你可以把它理解成乐高积木里的一个标准件。一个模型,就是由这些标准件拼起来的。
举个例子,一个简单的卷积层:
// 伪代码示意
output = conv2d(input, weight, bias, stride=1, padding=0)
这行代码背后,就是一个卷积算子。它接收输入特征图、权重和偏置,输出新的特征图。
核心要点: 算子 = 输入数据 + 计算逻辑 + 输出数据。三者缺一不可。
我在项目中遇到过不少新人,一上来就调模型结构,却连算子是什么都没搞清楚。结果调了半天,性能瓶颈根本不在模型结构上,而是某个算子实现太烂。嗯,这个坑我踩过,所以今天先把这个基础打牢。
算子优化的目标
好,知道了算子是什么,那优化它到底图个啥?
目标其实就两个:跑得快 和 占得少。
- 跑得快:延迟低,吞吐高。同样的模型,推理时间从100ms降到10ms,这就是优化。
- 占得少:显存占用低。一张卡能塞下更大的模型,或者跑更大的batch size。
你想想看,这两个目标有时候是矛盾的。跑得快往往需要更多的并行计算,而并行计算又可能带来更多的显存占用。怎么平衡?这就是咱们这门课的核心命题。
我的个人习惯: 拿到一个算子,先问自己三个问题——这个算子是计算密集型的还是访存密集型的?它的并行度够不够?有没有可以复用的中间结果?这三个问题想清楚了,优化方向就有了。
计算与访存的基本概念
这部分是重中之重。我见过太多人,算子优化做了半天,结果发现瓶颈根本不在计算上,而在数据搬运上。
计算量(FLOPs)
计算量,就是算子需要做多少次浮点运算。单位通常是FLOPs(Floating Point Operations)。
比如一个矩阵乘法 C = A × B,其中A是M×K,B是K×N,那计算量就是 2 × M × N × K(乘法和加法各算一次)。
// 矩阵乘法的计算量估算
int M = 1024, N = 1024, K = 1024;
long flops = 2L * M * N * K; // 约2.1G FLOPs
这个数字越大,说明算子越「吃计算」。但注意,计算量大不等于慢。如果你的硬件算力强,计算量大反而能更好地压满硬件。
访存量(Memory Access)
访存量,就是算子需要从显存里读写多少数据。单位通常是字节(Bytes)。
还是那个矩阵乘法,输入A、B和输出C,总访存量就是:
// 矩阵乘法的访存量估算
long memory_access = (M*K + K*N + M*N) * sizeof(float); // 假设float类型
这里有个关键点:计算和访存是两回事。计算是在芯片内部完成的,速度极快。访存是要去显存里搬数据,速度慢得多。你想想看,GPU的计算速度是TB/s级别,而显存带宽只有GB/s级别。差了整整一个数量级。
我曾经踩过一个坑: 优化一个卷积算子,我把计算量降了30%,满心欢喜以为能快30%。结果一测,延迟几乎没变。后来才发现,这个算子是访存密集型的,计算量根本不是瓶颈。白忙活了两天。所以,优化之前一定要先搞清楚瓶颈在哪。
计算强度(Arithmetic Intensity)
把计算量和访存量结合起来,就得到了一个非常重要的指标——计算强度。
公式很简单:
计算强度 = 计算量(FLOPs) / 访存量(Bytes)
单位是 FLOPs/Byte。这个值越大,说明算子越「计算密集」;越小,说明越「访存密集」。
一般来说:
| 计算强度 | 类型 | 典型算子 | 优化方向 |
|---|---|---|---|
| > 100 | 计算密集型 | 大矩阵乘法、大卷积 | 提升并行度、利用Tensor Core |
| 10 ~ 100 | 中等 | 中等尺寸卷积 | 计算访存都要优化 |
| < 10 | 访存密集型 | 激活函数、逐元素操作、小卷积 | 减少数据搬运、算子融合 |
我个人习惯,拿到一个算子先算一下它的计算强度。如果小于10,我基本不会花太多精力在计算优化上,而是想办法减少访存。如果大于100,那就要好好利用硬件的算力了。
核心逻辑:计算与访存的平衡
说了这么多,其实就一个核心逻辑:算子和硬件之间,存在一个「平衡点」。
这个平衡点,就是硬件的「屋顶线」(Roofline Model)上的那个拐点。当算子的计算强度大于这个拐点时,性能受限于计算能力;小于这个拐点时,性能受限于访存带宽。
我画了一张图,帮你直观理解这个关系:
你看这张图,横轴是计算强度,纵轴是性能。红色的斜线代表访存带宽的限制,蓝色的水平线代表计算能力的限制。两条线交汇的地方,就是那个平衡点。
如果一个算子的计算强度在拐点左边,比如ReLU,那它的性能就受限于访存带宽。你就算把计算单元跑冒烟了,性能也上不去,因为数据搬不过来。
如果一个算子的计算强度在拐点右边,比如大矩阵乘法,那它的性能就受限于计算能力。这时候你要想办法把计算单元喂饱,让它们别闲着。
避坑指南: 我曾经优化一个3x3的小卷积,花了三天时间手写汇编,把计算延迟降了20%。结果一测,整体延迟只降了2%。为什么?因为这个算子的计算强度只有8左右,属于访存密集型。我优化的那点计算时间,在访存开销面前根本不值一提。后来我改成算子融合,把三个小卷积合并成一个,访存直接减半,延迟降了40%。所以,方向比努力更重要。
小结
好了,今天的内容就这些。总结一下:
- 算子 是神经网络的基本计算单元,优化算子就是优化这些积木块。
- 优化目标 是跑得快(低延迟、高吞吐)和占得少(低显存)。
- 计算与访存 是算子的两个基本属性。计算强度决定了算子的类型,也决定了优化的方向。
- Roofline模型 帮你找到算子和硬件之间的平衡点,避免白费力气。
下一章,我会带你看看具体的算子优化技巧,比如如何利用Tensor Core、如何做算子融合、如何做内存布局优化。这些东西,都是我在实际项目中一点点磨出来的经验。咱们到时候细聊。
想要更多资料,联系可可