一、课程导论:为什么需要FFT加速卷积?卷积的数学本质与计算瓶颈
大家好,欢迎来到《FFT加速卷积层计算实践》的第一章。
先问大家一个问题:你训练一个ResNet-50,跑一次前向传播,大概有多少次乘法?
答案是——接近40亿次。其中90%以上都花在卷积层上。
我第一次跑这个数字的时候,说实话,吓了一跳。那时候我在做一个人脸识别项目,模型跑一次推理要200多毫秒,根本没法落地。后来我仔细一分析,好家伙,卷积层占了180毫秒。
所以问题来了:卷积为什么这么慢?有没有办法让它快起来?
嗯,这就是我们今天要聊的核心——FFT加速卷积。
1.1 卷积的数学本质:你到底在算什么?
先别急着写代码。我们得先搞清楚卷积到底在干嘛。
从数学上讲,二维卷积的定义是这样的:
(f * g)[i, j] = Σ_m Σ_n f[m, n] · g[i-m, j-n]
看着有点抽象对吧?我换个说法。
你有一个输入图像,比如224×224。你有一个卷积核,比如3×3。卷积操作就是:把卷积核滑过图像的每一个位置,在每个位置上做一次逐元素乘法,然后求和。
说白了,就是「滑动窗口 + 点积」。
那计算量有多大呢?
| 输入尺寸 | 卷积核尺寸 | 输出通道数 | 单层乘法次数 |
|---|---|---|---|
| 224×224 | 3×3 | 64 | 约9亿次 |
| 112×112 | 5×5 | 128 | 约20亿次 |
| 56×56 | 7×7 | 256 | 约39亿次 |
看到没?一个7×7的卷积核,一层就要算39亿次乘法。而一个ResNet-152有超过150层。
你想想看,这得算到什么时候?
1.2 计算瓶颈:为什么卷积这么慢?
我总结了一下,卷积慢的原因主要有三个:
- 计算密集:每个输出像素都要做K²次乘加,K是卷积核尺寸。K越大,计算量呈平方增长。
- 内存访问不友好:卷积核在图像上滑动时,相邻窗口之间有大量重复的数据加载。我做过一个测试,3×3卷积的重复加载率高达67%。
- 并行度受限:虽然卷积可以并行,但每个输出像素的计算依赖一个局部区域,导致GPU的线程利用率并不理想。
⚠️ 我曾经踩过的坑:
有一次我优化一个3D卷积网络,天真地以为用im2col + GEMM就能搞定。结果显存直接爆了——im2col把输入重复存储了9倍,一个256×256×64的特征图变成了256×256×576的矩阵。嗯,那次教训很深刻。
1.3 换个思路:时域卷积 = 频域乘积
好,问题摆在这了。有没有更聪明的办法?
有。而且这个办法来自一个非常经典的数学定理——卷积定理。
卷积定理说:时域上的卷积,等于频域上的逐元素乘积。
用公式表达就是:
f * g = IFFT( FFT(f) ⊙ FFT(g) )
其中⊙表示逐元素乘法。
这意味着什么?
原本你要做K²次乘加,现在只需要做3次FFT(前向两次,反向一次)加上一次逐元素乘法。
而FFT的复杂度是O(N log N),逐元素乘法是O(N)。
当卷积核尺寸K比较大时,这个优势就非常明显了。
关键结论:
当K > 7时,FFT方法开始优于直接卷积。K越大,优势越明显。
比如一个11×11的卷积核,FFT方法可以快5-10倍。
1.4 一个直观的例子
我习惯用一个小例子来说明问题。
假设你有一个4×4的输入,一个3×3的卷积核。
直接卷积:你需要滑动4次(横向)× 4次(纵向)= 16个位置,每个位置做9次乘法。总共144次乘法。
FFT方法:
- 对输入做FFT:O(16 log 16) ≈ 64次运算
- 对卷积核做FFT:同样64次
- 频域逐元素乘法:16次
- IFFT:64次
- 总共:约208次运算
咦?看起来反而更多了?
别急。这是因为例子太小了。当输入变成64×64,卷积核变成11×11时:
- 直接卷积:64×64×121 ≈ 50万次乘法
- FFT方法:3 × 64×64×log(64) + 64×64 ≈ 4.5万次运算
差了10倍以上。
💡 个人经验:
在实际项目中,我一般这样选:
- 3×3卷积:用直接卷积(im2col或Winograd)
- 5×5卷积:两者差不多,看具体硬件
- 7×7及以上:无脑上FFT
当然,这只是经验值。具体还是要profile一下。
1.5 本章小结
好,我们来捋一下今天的内容:
- 卷积的本质:滑动窗口 + 点积,计算量随卷积核尺寸平方增长
- 计算瓶颈:计算密集、内存访问不友好、并行度受限
- FFT加速原理:利用卷积定理,把时域卷积变成频域乘积
- 适用场景:大卷积核(K≥7)时优势明显
下一章,我会带大家手写一个FFT卷积的Python实现。到时候你会看到,代码其实没你想的那么复杂。
嗯,今天就到这。有问题欢迎在评论区讨论。
公众号:蓝海资料掘金营,微信deep3321