第四章:片上存储系统设计:SRAM与寄存器文件、多级存储层次、带宽匹配策略

大家好,欢迎来到第四章。这一章我们聊聊存储系统。

做深度学习加速器,说白了就是跟数据打交道。你算得再快,数据喂不进去,一切都是白搭。我见过太多团队,PE阵列设计得花里胡哨,结果一跑实际模型,性能连理论峰值的30%都不到。为什么?存储瓶颈卡死了。

所以这一章,我们聚焦三个核心问题:用什么存?怎么组织?怎么让数据流不卡顿?

4.1 存储元件选型:SRAM vs 寄存器文件

先解决最基础的问题——你手头有哪些存储元件可以用?

在加速器里,我们主要用两种:寄存器文件(Register File, RF)SRAM。很多人觉得这俩差不多,其实差别大了去了。

4.1.1 寄存器文件

寄存器文件,说白了就是一堆寄存器排排坐。它的特点是:

  • 速度快:读延迟通常在1个时钟周期以内
  • 面积大:一个6T的存储单元,比SRAM的bitcell大好几倍
  • 端口灵活:可以轻松做成多读多写端口

我个人习惯,只在最关键的路径上用RF。比如PE内部的累加器、权重缓存的第一级。其他地方,能省则省。

经验之谈: 我曾经在一个项目里,把所有中间结果都存到RF里,结果综合出来面积爆炸,频率死活上不去。后来改成SRAM+乒乓缓冲,面积降了40%,频率还提升了。

4.1.2 SRAM

SRAM是片上存储的主力军。它的密度比RF高得多,同样面积下能存更多数据。但代价是:

  • 访问延迟高:通常需要2-3个时钟周期才能读出数据
  • 端口受限:标准SRAM编译器生成的宏,通常只有1-2个读写端口
  • 需要时序闭合:SRAM的时序约束比RF复杂,稍不注意就出现hold violation

这里有个关键点:SRAM的读操作是同步的。你给出地址,下一个时钟周期数据才出来。这意味着你的流水线设计必须考虑这个延迟。

避坑指南: 我曾经在做一个卷积加速器时,想当然地以为SRAM读数据是组合逻辑输出,结果仿真时数据总是对不上。查了两天才发现,是SRAM的读延迟没处理好。记住:SRAM读数据,至少等一拍!

4.2 多级存储层次设计

好了,元件选好了,接下来怎么组织?

深度学习加速器的存储层次,我一般分成三级:

  1. L1:寄存器文件/本地缓冲 — 每个PE私有的,存当前计算需要的权重和输入
  2. L2:共享SRAM — 多个PE共享的,存特征图和权重块
  3. 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个权重。怎么办?

我的方案是这样的:

  1. 在L2和PE之间加一级L1缓冲,容量为4个权重块
  2. L2提前把权重读到L1中,PE从L1读
  3. L1用乒乓方式工作:一个块在给PE喂数据,另一个块在从L2读数据

这样,虽然L2的带宽只有8个/周期,但通过预取和乒乓,PE实际能感受到的带宽达到了32个/周期。当然,代价是增加了L1的面积和功耗。

这就是典型的用面积换带宽。在芯片设计里,这种trade-off随处可见。

4.4 存储系统的SVG结构图

下面这张图,是我画的一个典型深度学习加速器存储系统结构。你可以看到数据是怎么从片外DRAM,一层层流到PE的。

深度学习加速器多级存储系统结构 片外DRAM (L3) L2 共享SRAM 容量: 512KB - 2MB, 延迟: 2-3 cycles L1 缓冲 (PE0) RF: 1KB L1 缓冲 (PE1) RF: 1KB L1 缓冲 (PE2) RF: 1KB PE 计算阵列 MAC单元 × 64, 每个周期消耗 192 个数据 数据预取 乒乓缓冲 数据广播

从这张图里,你可以看到数据流动的路径:DRAM → L2 SRAM → L1缓冲 → PE阵列。每一级都在做一件事:把数据提前准备好,让下一级不空等

4.5 存储系统的RTL设计要点

最后,我们聊聊RTL实现时要注意的几个点。

4.5.1 SRAM的时序闭合

SRAM的时序,是RTL设计里最容易出问题的地方。我总结了几条经验:

  • 读操作:地址在时钟上升沿之前必须稳定。SRAM内部会锁存地址,然后在下个周期输出数据。所以你的读使能信号,必须提前一个周期准备好。
  • 写操作:数据和写使能必须在时钟上升沿之前稳定。注意,有些SRAM要求写使能在时钟沿之后还要保持一段时间(hold time)。
  • 时钟门控:如果SRAM没有被访问,建议用时钟门控关掉它。我曾经在一个项目里,SRAM的功耗占了芯片总功耗的40%。加上时钟门控后,降到了15%。
注意: 不同工艺的SRAM编译器,时序参数差别很大。我建议你在做RTL之前,先拿到目标工艺的SRAM datasheet,仔细看它的setup/hold time要求。不要想当然地认为所有SRAM都一样。

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的访问。
  • 带宽匹配:数据复用、乒乓缓冲、存储体交叉,这些手段都是为了解决「计算快、存储慢」的矛盾。

做存储系统设计,说白了就是在面积、功耗、带宽、延迟之间找平衡。没有完美的方案,只有最适合你应用场景的方案。

下一章,我们会进入控制通路的设计。到时候聊聊怎么让这些存储和计算单元,按照正确的节奏工作。


专注资料整理