第四章:片上存储系统设计:SRAM与寄存器文件、多级存储层次、带宽匹配策略
大家好,欢迎来到第四章。这一章我们聊聊存储系统。
做深度学习加速器,说白了就是跟数据打交道。你算得再快,数据喂不进去,一切都是白搭。我见过太多团队,PE阵列设计得花里胡哨,结果一跑实际模型,性能连理论峰值的30%都不到。为什么?存储瓶颈卡死了。
所以这一章,我们聚焦三个核心问题:用什么存?怎么组织?怎么让数据流不卡顿?
4.1 存储元件选型:SRAM vs 寄存器文件
先解决最基础的问题——你手头有哪些存储元件可以用?
在加速器里,我们主要用两种:寄存器文件(Register File, RF)和SRAM。很多人觉得这俩差不多,其实差别大了去了。
4.1.1 寄存器文件
寄存器文件,说白了就是一堆寄存器排排坐。它的特点是:
- 速度快:读延迟通常在1个时钟周期以内
- 面积大:一个6T的存储单元,比SRAM的bitcell大好几倍
- 端口灵活:可以轻松做成多读多写端口
我个人习惯,只在最关键的路径上用RF。比如PE内部的累加器、权重缓存的第一级。其他地方,能省则省。
4.1.2 SRAM
SRAM是片上存储的主力军。它的密度比RF高得多,同样面积下能存更多数据。但代价是:
- 访问延迟高:通常需要2-3个时钟周期才能读出数据
- 端口受限:标准SRAM编译器生成的宏,通常只有1-2个读写端口
- 需要时序闭合:SRAM的时序约束比RF复杂,稍不注意就出现hold violation
这里有个关键点:SRAM的读操作是同步的。你给出地址,下一个时钟周期数据才出来。这意味着你的流水线设计必须考虑这个延迟。
4.2 多级存储层次设计
好了,元件选好了,接下来怎么组织?
深度学习加速器的存储层次,我一般分成三级:
- L1:寄存器文件/本地缓冲 — 每个PE私有的,存当前计算需要的权重和输入
- L2:共享SRAM — 多个PE共享的,存特征图和权重块
- L3:片外DRAM — 整个芯片共享的,存完整的模型和数据
为什么要分这么多级?你想想看,如果所有数据都从片外DRAM读,带宽根本不够用。以ResNet-50为例,一次推理需要访问约8MB的权重数据。如果全部从DDR4读取,带宽需求轻松超过100GB/s。而实际上,DDR4的带宽也就30-40GB/s左右。
所以,多级存储的核心思想就是:把数据尽量往靠近计算单元的地方放。这就是所谓的「数据局部性」原理。
4.2.1 数据流与存储层次匹配
不同的数据流方式,对存储层次的要求也不同。我总结了一下:
| 数据流方式 | L1需求 | L2需求 | 典型应用 |
|---|---|---|---|
| 权重固定(Weight Stationary) | 大容量RF存权重 | 中等SRAM存输入 | 卷积层 |
| 输入固定(Input Stationary) | 大容量RF存输入 | 中等SRAM存权重 | 全连接层 |
| 输出固定(Output Stationary) | 大容量RF存部分和 | 大SRAM存中间结果 | 深度可分离卷积 |
嗯,这里要注意:没有一种数据流方式是万能的。我建议你在设计存储层次时,先想清楚你的目标应用场景是什么。如果是做通用加速器,那就要考虑支持多种数据流模式。
4.3 带宽匹配策略
这是整个存储系统设计中最头疼的部分。为什么?因为计算单元和存储单元之间的带宽,天然就不匹配。
举个例子:你的PE阵列有64个MAC单元,每个MAC每个周期需要读2个输入和1个权重。那么每个周期需要读3×64=192个数据。如果每个数据是16位,那就是384字节/周期。假设你的SRAM只有一个读端口,每个周期只能读64字节。那你的PE阵列利用率就只有64/384≈16.7%。
说白了,计算单元在等数据。这是最浪费性能的情况。
4.3.1 带宽匹配的常用手段
我常用的方法有这么几种:
- 数据复用:同一个数据被多个PE共享。比如卷积的权重,可以被多个输入通道复用。这样你只需要读一次权重,就能喂给多个PE。
- 乒乓缓冲:用两个buffer交替工作。一个buffer在给PE喂数据,另一个buffer在从L2读数据。这样读和算可以流水起来。
- 数据广播:一个数据同时发给多个PE。这要求你的数据通路支持广播模式。
- 存储体交叉:把SRAM分成多个bank,每个bank独立读写。这样可以在一个周期内读出多个数据。
4.3.2 一个实际案例
我在做某款AI芯片时,遇到了一个典型问题:PE阵列需要每个周期读32个权重,但L2 SRAM每个周期只能提供8个权重。怎么办?
我的方案是这样的:
- 在L2和PE之间加一级L1缓冲,容量为4个权重块
- L2提前把权重读到L1中,PE从L1读
- L1用乒乓方式工作:一个块在给PE喂数据,另一个块在从L2读数据
这样,虽然L2的带宽只有8个/周期,但通过预取和乒乓,PE实际能感受到的带宽达到了32个/周期。当然,代价是增加了L1的面积和功耗。
这就是典型的用面积换带宽。在芯片设计里,这种trade-off随处可见。
4.4 存储系统的SVG结构图
下面这张图,是我画的一个典型深度学习加速器存储系统结构。你可以看到数据是怎么从片外DRAM,一层层流到PE的。
从这张图里,你可以看到数据流动的路径:DRAM → L2 SRAM → L1缓冲 → PE阵列。每一级都在做一件事:把数据提前准备好,让下一级不空等。
4.5 存储系统的RTL设计要点
最后,我们聊聊RTL实现时要注意的几个点。
4.5.1 SRAM的时序闭合
SRAM的时序,是RTL设计里最容易出问题的地方。我总结了几条经验:
- 读操作:地址在时钟上升沿之前必须稳定。SRAM内部会锁存地址,然后在下个周期输出数据。所以你的读使能信号,必须提前一个周期准备好。
- 写操作:数据和写使能必须在时钟上升沿之前稳定。注意,有些SRAM要求写使能在时钟沿之后还要保持一段时间(hold time)。
- 时钟门控:如果SRAM没有被访问,建议用时钟门控关掉它。我曾经在一个项目里,SRAM的功耗占了芯片总功耗的40%。加上时钟门控后,降到了15%。
4.5.2 寄存器文件的实现
寄存器文件,我一般用行为级描述,让综合工具自己去推断。比如:
// 一个双读单写的寄存器文件
module regfile #(
parameter DEPTH = 32,
parameter WIDTH = 16
)(
input clk,
input rst_n,
// 写端口
input we,
input [clog2(DEPTH)-1:0] waddr,
input [WIDTH-1:0] wdata,
// 读端口0
input [clog2(DEPTH)-1:0] raddr0,
output [WIDTH-1:0] rdata0,
// 读端口1
input [clog2(DEPTH)-1:0] raddr1,
output [WIDTH-1:0] rdata1
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
// 写操作
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 初始化
end else if (we) begin
mem[waddr] <= wdata;
end
end
// 读操作(组合逻辑输出)
assign rdata0 = mem[raddr0];
assign rdata1 = mem[raddr1];
endmodule
嗯,这里要注意:读操作是组合逻辑的。这意味着rdata0和rdata1会随着raddr0和raddr1的变化而立即变化。如果你的设计需要同步读,那就要在输出加一级寄存器。
4.5.3 带宽匹配的RTL实现
带宽匹配,说白了就是用流水线和缓冲来解耦。我常用的结构是这样的:
// 乒乓缓冲控制器
module pingpong_ctrl #(
parameter DATA_WIDTH = 64,
parameter BUF_DEPTH = 16
)(
input clk,
input rst_n,
// 写端口(来自L2)
input wr_en,
input [DATA_WIDTH-1:0] wr_data,
// 读端口(到PE)
input rd_en,
output [DATA_WIDTH-1:0] rd_data,
output buf_empty,
output buf_full
);
reg sel; // 0: buffer A active, 1: buffer B active
reg [DATA_WIDTH-1:0] buf_a [0:BUF_DEPTH-1];
reg [DATA_WIDTH-1:0] buf_b [0:BUF_DEPTH-1];
// ... 控制逻辑
// 核心思想:一个buffer在写,另一个在读
// 当写满或读空时,切换sel
endmodule
这个乒乓缓冲,是我用得最多的带宽匹配手段。它简单、可靠,而且容易验证。
4.6 本章小结
这一章我们聊了片上存储系统的三个核心问题:
- 存储元件:RF快但面积大,SRAM密度高但延迟大。选哪个,看你的性能目标和面积预算。
- 多级存储层次:L1/L2/L3三级结构,核心是利用数据局部性,减少对片外DRAM的访问。
- 带宽匹配:数据复用、乒乓缓冲、存储体交叉,这些手段都是为了解决「计算快、存储慢」的矛盾。
做存储系统设计,说白了就是在面积、功耗、带宽、延迟之间找平衡。没有完美的方案,只有最适合你应用场景的方案。
下一章,我们会进入控制通路的设计。到时候聊聊怎么让这些存储和计算单元,按照正确的节奏工作。