第二讲:局部性原理——时间局部性与空间局部性在AI计算中的应用
局部性原理,说白了就是程序访问内存时的一种“偷懒”规律。你想想看,CPU访问内存的速度和访问缓存的速度差了两个数量级,如果每次都要去主存拿数据,那AI芯片的性能就彻底完蛋了。我刚开始做AI芯片设计时,总觉得缓存越大越好,后来才发现,不懂局部性原理,给你1GB缓存也是白搭。
1. 时间局部性:同一个数据,反复用
时间局部性的核心思想很简单:刚刚用过的数据,很可能马上还要再用一次。这在AI计算中太常见了。
举个例子,卷积神经网络里的权重参数。你在做卷积计算时,同一个卷积核的权重会被反复用到不同的输入特征图上。我遇到过这样一个项目:一个7×7的卷积核,输入是224×224的特征图。如果不做缓存优化,每次计算都要从主存里把权重搬出来,那延迟简直不敢想。
关键点:时间局部性要求我们把“热数据”留在缓存里,别急着踢出去。
我个人习惯在写AI算子时,先分析一下数据的“复用距离”。什么叫复用距离?就是同一个数据两次被访问之间,间隔了多少次其他访问。如果复用距离小于缓存大小,那时间局部性就能发挥得很好。
2. 空间局部性:相邻数据,一起拿
空间局部性就更直观了:你用了一个地址的数据,它旁边的数据大概率也会被用到。这背后的逻辑是,内存是按块(cache line)读取的,你读一个字节,实际上把周围64字节都搬进了缓存。
我记得有一次优化矩阵乘法,发现性能死活上不去。后来一查,原来是数据在内存里是按行存储的,但我的算法是按列访问的。这就导致每次访问都要重新加载cache line,空间局部性完全被破坏了。
我的建议:写AI算子时,尽量保证数据访问是连续的。比如遍历一个二维数组,先按行遍历,别按列遍历。这听起来简单,但我在代码评审时经常看到有人犯这个错。
3. AI计算中的局部性实战
咱们来看看在AI芯片里,局部性原理具体怎么用。我以卷积运算为例,画一张图来说明。
从这张图能看出来,卷积运算天然就具备两种局部性:
- 空间局部性:当前窗口的相邻像素,在滑动窗口时马上就会被用到。所以一次性把整个窗口的数据加载到缓存里,效率最高。
- 时间局部性:卷积核的权重在整个特征图上滑动时,会被反复使用。我建议把权重常驻在缓存里,别让它被踢出去。
4. 避坑指南:我曾经踩过的坑
我曾经在一个项目里,为了省缓存空间,把卷积核权重分批次加载。结果每次加载都要等主存,性能直接掉了40%。后来我才意识到,权重虽然占空间,但它的时间局部性太好了,必须留在缓存里。
还有一个坑是关于数据对齐的。我记得有一次,输入数据的起始地址没有按64字节对齐,结果每次加载cache line都要多读一些无用数据,空间局部性大打折扣。从那以后,我写代码时都会强制数据对齐。
注意:不要以为局部性原理只对CPU有效。在AI芯片的NPU、TPU里,局部性同样重要。甚至可以说,AI芯片的加速效果,很大程度上取决于你能不能把局部性原理用好。
5. 量化分析:局部性对性能的影响
咱们用数据说话。假设一个AI芯片的缓存命中率从90%提升到99%,性能能提升多少?我算给你看:
| 缓存命中率 | 平均访问延迟(周期) | 相对性能 |
|---|---|---|
| 90% | 1 + 0.1 × 100 = 11 | 1.0x |
| 95% | 1 + 0.05 × 100 = 6 | 1.83x |
| 99% | 1 + 0.01 × 100 = 2 | 5.5x |
看到了吧?命中率从90%到99%,性能提升了5.5倍。这就是局部性原理的价值所在。我见过很多团队花大量精力去优化计算单元,却忽略了数据搬移的开销,结果事倍功半。
6. 实战技巧:如何提升局部性
最后分享几个我在项目中常用的技巧:
- 数据重排:如果原始数据访问模式不连续,可以在加载时重新排列数据。比如把按列访问改成按行访问,或者做一次矩阵转置。
- 分块(tiling):把大矩阵切成小块,让每个小块的数据都能塞进缓存。我习惯把块大小设为缓存大小的1/4,留点余量给其他数据。
- 预取(prefetch):在数据被用到之前,提前把它加载到缓存里。这需要你对数据访问模式有预判,我一般会在循环里加几条预取指令。
- 缓存锁定:对于时间局部性特别好的数据(比如卷积核权重),可以锁定在缓存里,防止被替换出去。不过这个功能不是所有芯片都支持,得看具体硬件。
小提示:优化局部性时,别光盯着缓存命中率看。有时候命中率上去了,但数据搬移的总量没变,性能提升有限。我建议同时关注“有效带宽利用率”,这才是真正的性能指标。
嗯,关于局部性原理,今天就聊这么多。记住一句话:缓存不是万能的,但不懂局部性原理是万万不能的。下次写AI算子时,先想想你的数据访问模式,再动手编码,效果会好很多。