1. 缓存基础与存储层次:为什么需要缓存?
大家好,我是你们这堂课的主讲人。今天咱们聊聊缓存,这个在算子映射里绕不开的话题。
说实话,我刚开始做AI芯片那会儿,也犯过嘀咕:不就是个存储吗?直接拿DRAM当主存用不就完了?后来被现实狠狠教育了一顿。你想想看,一个卷积算子在DRAM里读数据,和从缓存里读,速度能差出两个数量级。这可不是闹着玩的。
1.1 存储层次的延迟差异
先看一组数据。我习惯把这些数字刻在脑子里,因为做算子映射时,每一步决策都跟它有关。
| 存储层级 | 典型延迟 | 相对CPU周期 | 容量 |
|---|---|---|---|
| 寄存器 | ~0.3 ns | 1 cycle | 几十字节 |
| L1 Cache (SRAM) | ~1 ns | 3-4 cycles | 32-64 KB |
| L2 Cache (SRAM) | ~3-5 ns | 10-20 cycles | 256-512 KB |
| L3 Cache (SRAM) | ~10-15 ns | 30-50 cycles | 几MB |
| 主存 (DRAM) | ~50-100 ns | 200-400 cycles | 几GB |
看到没?从L1到主存,延迟差了将近100倍。我在项目中遇到过最夸张的一次,一个矩阵乘法算子因为缓存命中率从90%掉到30%,性能直接腰斩。嗯,从那以后我再也不敢小看缓存了。
1.2 SRAM vs DRAM:为什么差这么多?
说白了,SRAM和DRAM的差异,根子在电路结构上。
- SRAM(静态随机存取存储器):用6个晶体管存1个bit。速度快,但面积大、功耗高。适合做缓存。
- DRAM(动态随机存取存储器):用1个晶体管+1个电容存1个bit。密度高、成本低,但需要定期刷新,速度慢。适合做主存。
你想想看,SRAM一个bit要6个管子,DRAM只要1个。同样面积下,DRAM容量能大出五六倍。但代价就是慢——电容充放电需要时间,还得时不时刷新一下,不然数据就丢了。
核心观点:缓存存在的根本原因,就是SRAM比DRAM快,但贵。所以我们用少量SRAM做缓存,把最常用的数据放进去,大部分数据还是留在DRAM里。
1.3 局部性原理:缓存能工作的理论基础
缓存不是万能的。它之所以有效,全靠程序的局部性原理。我建议每个做算子映射的工程师,都把这两个概念刻在脑子里。
时间局部性
如果一个数据被访问了,那么它很可能在不久的将来再次被访问。
举个例子:你在写一个卷积循环,权重矩阵会被反复读取。这就是时间局部性。缓存会把这个权重矩阵留在L1里,下次用的时候直接命中。
空间局部性
如果一个数据被访问了,那么它附近的数据也很可能被访问。
还是卷积的例子:你读了一个像素点,接下来大概率会读它旁边的像素点。缓存会把一整块数据(比如一个cache line,通常是64字节)都拉进来。这样你读下一个像素时,它已经在缓存里了。
我的经验:做算子映射时,我习惯先分析数据流的局部性特征。如果时间局部性强,就尽量把数据留在缓存里;如果空间局部性强,就调整数据布局,让连续访问的数据在内存里也连续存放。
1.4 缓存命中的代价
这里有个概念叫缓存命中率。命中率越高,性能越好。但一旦发生缓存缺失,代价就大了。
我曾经在一个项目中,因为没注意数据对齐,导致大量缓存缺失。一个简单的加法算子,延迟从2个周期飙到了50个周期。你想想看,25倍的差距,这谁受得了?
缓存缺失的典型场景:
- 强制缺失:第一次访问某个数据,缓存里肯定没有。这是不可避免的。
- 容量缺失:缓存太小,装不下所有需要的数据。这时候就得考虑分块(tiling)了。
- 冲突缺失:多个数据映射到同一个缓存行,互相踢来踢去。这通常是因为地址映射策略不好。
避坑指南:我曾经在映射一个深度可分离卷积时,没注意输入特征图和权重的缓存冲突。结果两个数据反复互相踢,命中率从95%掉到了40%。后来调整了数据排布,把权重和特征图放在不同的缓存组里,问题才解决。
1.5 知识体系总览
下面这张图,是我自己总结的缓存基础知识框架。你把它理解了,后面讲算子映射时就能跟上节奏。
1.6 小结
好了,这一章的内容就这些。说白了,缓存就是利用SRAM的速度优势,配合程序的局部性原理,把最常用的数据放在离CPU最近的地方。
你想想看,一个算子映射做得好不好,很大程度上就看你能不能把数据留在缓存里。后面几章我们会深入讲具体的映射策略,但今天这些基础概念,是绕不开的。
嗯,今天就到这儿。记住一句话:缓存命中率,就是算子的生命线。
课后思考:你手头正在做的算子,数据访问模式是时间局部性强,还是空间局部性强?如果是后者,你会怎么调整数据布局?