3、片上存储层次:全局缓冲区、局部寄存器文件、存储容量与带宽权衡
好,咱们今天聊聊 Eyeriss 架构里一个特别实在的话题——片上存储。说白了,就是数据在芯片内部怎么放、怎么搬。很多做加速器的朋友,一开始都盯着计算单元猛优化,结果发现性能瓶颈根本不在算力,而在数据喂不进去。我当年就吃过这个亏。
3.1 为什么存储层次这么重要?
你想想看,一个卷积层要读多少数据?权重、输入特征图、中间结果,动不动就是几兆甚至几十兆。而片上 SRAM 就那么点地方,DRAM 带宽又金贵。所以 Eyeriss 设计了一套三级存储体系:全局缓冲区(Global Buffer)、行缓冲区(Row Buffer) 和 寄存器文件(Register File)。每一级都有自己的使命。
核心原则:数据越往计算单元靠近,访问速度越快,但容量越小。我们的目标就是让数据尽量待在离计算近的地方,少去 DRAM 里折腾。
我个人习惯把这三层比作一个水桶系统:全局缓冲区是水塔,行缓冲区是水管,寄存器文件就是水龙头。水塔要大,但出水慢;水龙头要快,但存不了多少水。你得算好流量,不然要么水塔空转,要么水龙头干等。
3.2 全局缓冲区(Global Buffer)
全局缓冲区是片上最大的存储块,通常用 SRAM 实现。在 Eyeriss 里,它负责存放整个输入特征图的一个 tile 和对应的权重子集。为什么不是整张图?因为放不下啊。1080p 的输入,单通道就 2MB 了,加上权重,片上 SRAM 根本扛不住。
容量怎么定? 我记得在做一个 4K 视频处理项目时,我们反复调过这个参数。经验公式是这样的:
- 至少能放下 2-3 行输入特征图(配合行缓冲区做滑动窗口)
- 权重 buffer 要能装下一个卷积核的全部权重
- 给输出特征图留出足够的累加空间
Eyeriss 的全局缓冲区配置大概是 108KB 到 192KB 这个范围。你可能会问,为什么不是 256KB 或者更大?因为面积和功耗会爆炸。每增加 1KB SRAM,面积增加约 0.01mm²(28nm 工艺),功耗增加约 0.5mW。这个账得算清楚。
避坑指南:我曾经在一个项目里把全局缓冲区做得特别大,想着“反正面积够用”。结果后端布局布线时发现,SRAM 离计算单元太远,走线延迟直接把时钟频率拉低了 20%。嗯,从那以后我学会了:存储不是越大越好,位置和访问模式同样重要。
3.3 行缓冲区(Row Buffer)
行缓冲区是 Eyeriss 的一个特色设计。它本质上是一个小型的 FIFO,专门用来缓存输入特征图的连续几行。为什么需要它?因为卷积核是滑动窗口,每次移动一步,但数据是按行存储的。如果没有行缓冲区,每次滑动都要从全局缓冲区重新读一整行,带宽根本扛不住。
行缓冲区的大小取决于卷积核的尺寸。3×3 卷积需要 3 行,5×5 需要 5 行。每行宽度等于特征图的宽度。举个例子:输入是 224×224,3×3 卷积,行缓冲区就需要 3×224 = 672 个数据。每个数据 16 位,那就是 1344 字节。看起来不大,但别忘了,这是每个 PE 阵列都要配一份的。
我建议在设计时,行缓冲区的大小要留一点余量。因为有些网络会用空洞卷积(dilated convolution),步长不是 1,这时候行缓冲区的行数要相应增加。我在做语义分割模型部署时就踩过这个坑,空洞率 2 的 3×3 卷积,实际需要 5 行数据。
3.4 寄存器文件(Register File)
寄存器文件是离计算单元最近的存储,通常就在 PE 内部。Eyeriss 的每个 PE 都有自己的寄存器文件,用来存放当前计算需要的权重和输入数据。它的容量很小,一般就几十个字节,但速度极快——一个时钟周期就能读写。
寄存器文件的设计要点是:
- 双端口或三端口: 一个读端口给计算用,一个写端口给数据加载用,避免读写冲突
- 深度匹配计算流水线: 比如 PE 做乘加需要 3 级流水,寄存器文件就要能存 3 组数据
- 支持广播: 同一个权重可以同时发给多个 PE,减少重复加载
注意:寄存器文件虽然快,但面积开销很大。一个 32×32 的寄存器文件,面积差不多是同等容量 SRAM 的 3-4 倍。所以别贪多,够用就行。我见过有人给每个 PE 配了 1KB 寄存器文件,结果芯片面积直接翻倍,性能却没提升多少。
3.5 存储容量与带宽的权衡
这是整个存储层次设计的核心。说白了,就是用容量换带宽,用带宽换容量。我给你画个图,你就明白了。
这张图很直观:从上到下,容量越来越小,但带宽越来越大。DRAM 带宽是瓶颈,所以我们要尽量把数据搬到片上。但片上 SRAM 容量有限,所以又得精打细算。
3.6 实际设计中的权衡策略
那具体怎么权衡呢?我总结了几条实战经验:
| 场景 | 策略 | 效果 |
|---|---|---|
| 权重复用率高(如全连接层) | 增大权重 buffer,减少重复加载 | 带宽降低 30-50% |
| 输入特征图复用率高(如卷积层) | 增大行缓冲区,支持更大滑动窗口 | 带宽降低 40-60% |
| 输出累加中间结果多 | 增大输出 buffer,减少写回 | 带宽降低 20-30% |
| 多通道并行计算 | 增加寄存器文件深度,支持更多通道 | 计算效率提升 50%+ |
举个例子: 我在部署 ResNet-50 时,发现第一层卷积(7×7,步长 2)对行缓冲区压力特别大。7×7 卷积需要 7 行数据,而输入是 224×224,每行 224 个像素。行缓冲区要 7×224 = 1568 个数据。如果每个数据 16 位,就是 3136 字节。而我的行缓冲区只有 2KB,根本不够。怎么办?
我当时的做法是:把输入特征图做 tile 切分,每次只处理 112 列宽度的数据。这样行缓冲区只需要 7×112 = 784 个数据,约 1.5KB,刚好放下。代价是全局缓冲区的带宽压力增加了,因为要多次读取同一行数据。但整体来看,这个 trade-off 是划算的。
技巧: 在做存储容量规划时,我建议先跑一遍网络的数据流分析。统计每一层的数据复用次数、访问模式、中间结果大小。然后根据这些数据,反推各级存储的最小容量。别拍脑袋定大小,数据会说话。
3.7 带宽计算的实用公式
最后,分享一个我常用的带宽估算公式。假设你要处理一个卷积层:
总数据量 = 输入特征图大小 + 权重大小 + 输出特征图大小
所需带宽 = 总数据量 / 处理时间
实际中,还要考虑数据复用:
有效带宽 = 总数据量 / (复用次数 × 处理时间)
举个例子:
输入:56×56×64,权重:3×3×64×128,输出:56×56×128
总数据量 = 56×56×64 + 3×3×64×128 + 56×56×128 = 200704 + 73728 + 401408 = 675840 字节
如果处理时间 1ms,所需带宽 = 675840 / 0.001 = 675.84 MB/s
但如果有行缓冲区复用输入数据,复用次数 = 3×3 = 9
有效带宽 = 675.84 / 9 = 75.09 MB/s
你看,有了行缓冲区,带宽需求直接降了一个数量级。这就是存储层次设计的价值所在。
再次提醒: 带宽计算时,别忘了考虑数据对齐和 burst 传输的影响。DRAM 的 burst 长度通常是 8 或 16,如果你的数据不是 burst 对齐的,实际带宽会打折扣。我曾经因为没注意这个,仿真时带宽够用,上板后性能直接腰斩。血的教训。