FPGA加速CNN:脉动阵列原理与实战详解

FPGA CNN 加速器原理与实现详解

目录


一、核心原理

1.1 为什么用 FPGA 加速 CNN?

CNN(卷积神经网络)的计算特点:

  • 计算密集:大量乘加运算(MAC)
  • 数据复用:卷积核在特征图上滑动,输入数据被重复使用
  • 并行性强:不同通道、不同卷积核可并行计算
  • 访存密集:特征图和权重数据量大

FPGA 的优势

  • 定制化并行架构:可根据 CNN 层的特点设计专用硬件
  • 流水线深度可控:平衡延迟与吞吐
  • 低功耗:相比 GPU,相同算力下功耗更低
  • 灵活性:支持不同网络结构(ResNet / MobileNet / YOLO)

二、脉动阵列(Systolic Array)核心设计

2.1 核心思想

类比心脏泵血:数据像血液一样在阵列中有节奏地流动(Systolic = 心脏收缩)。

设计目标

  1. 最大化数据复用:每个数据元素在多个 PE 中被使用
  2. 减少全局通信:数据只在相邻 PE 间传递
  3. 规律的数据流:便于硬件实现和时序优化

2.2 4×4 脉动阵列物理拓扑

text 复制代码
         inputs[0]   inputs[1]   inputs[2]   inputs[3]
            │           │           │           │
            ↓           ↓           ↓           ↓
weights[0]→ PE00 ────→ PE01 ────→ PE02 ────→ PE03
            │           │           │           │
weights[1]→ PE10 ────→ PE11 ────→ PE12 ────→ PE13
            │           │           │           │
weights[2]→ PE20 ────→ PE21 ────→ PE22 ────→ PE23
            │           │           │           │
weights[3]→ PE30 ────→ PE31 ────→ PE32 ────→ PE33
            │           │           │           │
            ↓           ↓           ↓           ↓
         outputs[0] outputs[1] outputs[2] outputs[3]

数据流动方向

  • 横向(→) :激活值从左向右传递(通过 out_a
  • 纵向(↓) :部分和从上向下累加(通过 sum_out
  • 权重(→):每行广播,不在 PE 间流动(Weight-Stationary)

2.3 嵌套 for 循环生成 PE 网格

verilog 复制代码
generate
    for (i = 0; i < N; i = i + 1) begin : row_gen      // 外层:行 (0..N-1)
        for (j = 0; j < N; j = j + 1) begin : col_gen  // 内层:列 (0..N-1)

            pe #(
                .DATA_WIDTH(DATA_WIDTH),
                .ACC_WIDTH(ACC_WIDTH)
            ) pe_inst (
                .clk    (clk),
                .rst_n  (rst_n),

                // 激活值:第0行从外部输入,其余行从上方 PE 接收
                .in_a   ( (i == 0) ? inputs[j] : a_wire[i-1][j] ),
                .out_a  ( a_wire[i][j] ),

                // 权重:每行广播
                .in_w   ( weights[i] ),

                // 部分和:第0行初始化为0,其余行从上方累加
                .sum_in ( (i == 0) ? {ACC_WIDTH{1'b0}} : s_wire[i-1][j] ),
                .sum_out( s_wire[i][j] )
            );

        end
    end
endgenerate

// 输出取最后一行的累加结果
genvar k;
generate
    for (k = 0; k < N; k = k + 1) begin : out_gen
        assign outputs[k] = s_wire[N-1][k];
    end
endgenerate

2.4 单个 PE(Processing Element)内部结构

verilog 复制代码
module pe #(
    parameter DATA_WIDTH = 8,
    parameter ACC_WIDTH  = 32
) (
    input  wire                        clk,
    input  wire                        rst_n,

    // 水平输入(激活值)
    input  wire signed [DATA_WIDTH-1:0] in_a,
    output reg  signed [DATA_WIDTH-1:0] out_a,   // 传给右边 PE

    // 权重(可静态加载)
    input  wire signed [DATA_WIDTH-1:0] in_w,

    // 部分和累加(垂直方向)
    input  wire signed [ACC_WIDTH-1:0]  sum_in,
    output reg  signed [ACC_WIDTH-1:0]  sum_out
);

    reg signed [DATA_WIDTH-1:0] a_reg;
    reg signed [DATA_WIDTH-1:0] w_reg;
    reg signed [ACC_WIDTH-1:0]  mult_reg;
    reg signed [ACC_WIDTH-1:0]  sum_reg;

    // Stage 1: 寄存输入 + 乘法(映射到 DSP48)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            a_reg    <= 0;
            w_reg    <= 0;
            mult_reg <= 0;
        end else begin
            a_reg    <= in_a;
            w_reg    <= in_w;
            mult_reg <= $signed(a_reg) * $signed(w_reg);
        end
    end

    // Stage 2: 累加 + 传递
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            out_a   <= 0;
            sum_reg <= 0;
            sum_out <= 0;
        end else begin
            out_a   <= a_reg;               // 激活值向右传递
            sum_reg <= sum_in + mult_reg;   // 累加
            sum_out <= sum_reg;             // 部分和向下传递
        end
    end

endmodule

关键设计点

  1. 乘法映射到 DSP48:避免 LUT 实现的长延迟
  2. 两级流水线:乘法 1 周期,累加 1 周期
  3. 数据传递寄存:保证时序和同步

三、数据流动的时空特性

3.1 矩阵乘法映射

计算 C = A × B(4×4):

text 复制代码
C[i][j] = Σ(k=0..3) A[i][k] * B[k][j]

3.2 时空图(Space-Time Diagram)

text 复制代码
空间维度(PE 列)
  ↓
0  1  2  3
┌──┬──┬──┬──┐
│A0│  │  │  │  T=0: 输入 A[0][0]
├──┼──┼──┼──┤
│A1│A0│  │  │  T=1: A[0][0] 右移,A[1][0] 进入
├──┼──┼──┼──┤
│A2│A1│A0│  │  T=2: 继续传播
├──┼──┼──┼──┤
│A3│A2│A1│A0│  T=3: 对角线填满
└──┴──┴──┴──┘
 → 时间维度

关键特性

  • 波前传播:数据以对角线形式从左上扫向右下
  • 延迟 = N + Pipeline_depth:4×4 阵列需要 4+2=6 个周期
  • 吞吐率 = 1 结果/周期(稳态)

四、CNN 卷积层的映射策略

4.1 卷积操作的本质(六重循环)

python 复制代码
for oc in output_channels:          # 输出通道
    for y in output_height:         # 输出行
        for x in output_width:      # 输出列
            for ic in input_channels:      # 输入通道
                for ky in kernel_height:   # 卷积核行
                    for kx in kernel_width:# 卷积核列
                        MAC operation

4.2 循环重排与并行策略

策略 说明 适用场景
输出通道并行 多个阵列,各算 1 个输出通道 通道数多
输入通道展开 每个 PE 累加多个输入通道 深层网络
时间复用 单阵列分时处理多通道 资源受限

4.3 3×3 卷积加速器示例

verilog 复制代码
module conv3x3_accelerator #(
    parameter INPUT_CHANNELS  = 64,
    parameter OUTPUT_CHANNELS = 128,
    parameter PARALLEL_OC     = 16   // 并行处理 16 个输出通道
) (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  pixel_stream,
    input  wire        pixel_valid,
    output wire [31:0] output_stream [0:PARALLEL_OC-1],
    output wire        output_valid
);

    // 1. 行缓存生成 3×3 滑动窗口
    wire [7:0] window [0:2][0:2];
    wire       window_valid;

    line_buffer #(
        .WIDTH(8),
        .IMAGE_WIDTH(224)
    ) line_buf_inst (
        .clk         (clk),
        .pixel_in    (pixel_stream),
        .valid_in    (pixel_valid),
        .window_out  (window),
        .window_valid(window_valid)
    );

    // 2. 并行 MAC 阵列
    genvar oc;
    generate
        for (oc = 0; oc < PARALLEL_OC; oc = oc + 1) begin : pe_array
            wire [7:0]  inputs  [0:8];
            wire [7:0]  weights [0:8];
            wire [31:0] mac_result;

            mac_array #(.N(9)) mac_inst (
                .clk     (clk),
                .inputs  (inputs),
                .weights (weights),
                .result  (mac_result)
            );

            assign output_stream[oc] = mac_result;
        end
    endgenerate

endmodule

五、存储层次与数据复用

5.1 存储金字塔

text 复制代码
┌────────────────┐
│   DDR / HBM    │  大容量(GB) 低带宽 高延迟
└───────┬────────┘
        │ DMA Burst
┌───────↓────────┐
│  BRAM Buffer   │  中容量(MB) 中带宽
└───────┬────────┘
        │ 行/块读取
┌───────↓────────┐
│  寄存器阵列     │  小容量(KB) 高带宽
└───────┬────────┘
        │ 每周期访问
┌───────↓────────┐
│  PE 寄存器     │  最小(Byte) 零延迟
└────────────────┘

5.2 乒乓缓存(Ping-Pong Buffer)

verilog 复制代码
module feature_map_buffer #(
    parameter TILE_HEIGHT = 16,
    parameter TILE_WIDTH  = 16,
    parameter CHANNELS    = 64
) (
    input  wire         clk,
    input  wire [127:0] axi_tdata,
    input  wire         axi_tvalid,
    output reg          axi_tready,
    output wire [7:0]   pixel_out,
    input  wire         pixel_read
);

    (* ram_style = "block" *)
    reg [7:0] buffer_A [0:TILE_HEIGHT*TILE_WIDTH*CHANNELS-1];
    reg [7:0] buffer_B [0:TILE_HEIGHT*TILE_WIDTH*CHANNELS-1];

    reg ping_pong;  // 0: A写B读, 1: B写A读

    assign pixel_out = ping_pong ? buffer_A[read_addr] : buffer_B[read_addr];

endmodule

5.3 DDR 访问优化(参考知识库时序约束)

tcl 复制代码
# DDR UI 时钟与用户逻辑时钟 CDC
set_clock_groups -asynchronous \
    -group [get_clocks ui_clk] \
    -group [get_clocks sys_clk]

# 跨时钟域 FIFO 约束
set_max_delay -datapath_only \
    -from [get_clocks sys_clk] \
    -to   [get_clocks ui_clk] 4.0

六、完整 CNN 加速器架构

6.1 顶层架构

text 复制代码
┌────────────────────────────────────────────────────────┐
│                    CNN Accelerator                     │
├───────────┬────────────────────────────────────────────┤
│  Control  │              Data Path                     │
│   Unit    │  ┌──────────────────────────────────────┐  │
│           │  │      Input Feature Map Buffer        │  │
│  层参数    │  │    (Line Buffer + Tile Cache)        │  │
│  地址生成  │  └──────────────┬───────────────────────┘  │
│  状态机    │  ┌──────────────↓───────────────────────┐  │
│           │  │    Systolic Array (16×16 PEs)        │  │
│           │  └──────────────┬───────────────────────┘  │
│           │  ┌──────────────↓───────────────────────┐  │
│           │  │      Activation (ReLU)               │  │
│           │  └──────────────┬───────────────────────┘  │
│           │  ┌──────────────↓───────────────────────┐  │
│           │  │      Pooling (Max/Avg)               │  │
│           │  └──────────────┬───────────────────────┘  │
│           │  ┌──────────────↓───────────────────────┐  │
│           │  │    Output Feature Map Buffer         │  │
│           │  └──────────────────────────────────────┘  │
└───────────┴────────────────────────────────────────────┘
      ↕                             ↕
  AXI-Lite                      AXI-MM (DDR)
 (配置接口)                    (数据传输)

6.2 控制状态机

verilog 复制代码
typedef enum logic [3:0] {
    IDLE,
    LOAD_WEIGHTS,   // 加载权重
    LOAD_INPUT,     // 加载输入特征图
    COMPUTE_CONV,   // 卷积计算
    ACTIVATION,     // 激活函数
    POOLING,        // 池化
    STORE_OUTPUT,   // 写回 DDR
    LAYER_DONE
} state_t;

always @(*) begin
    next_state = state;
    case (state)
        IDLE:         if (layer_start)      next_state = LOAD_WEIGHTS;
        LOAD_WEIGHTS: if (weight_load_done) next_state = LOAD_INPUT;
        LOAD_INPUT:   if (input_tile_ready) next_state = COMPUTE_CONV;
        COMPUTE_CONV: if (conv_done)        next_state = ACTIVATION;
        ACTIVATION:   if (act_done)         next_state = POOLING;
        POOLING:      if (pool_done)        next_state = STORE_OUTPUT;
        STORE_OUTPUT: next_state = all_tiles_done ? LAYER_DONE : LOAD_INPUT;
        LAYER_DONE:   next_state = IDLE;
    endcase
end

七、性能评估与优化

7.1 性能指标

理论峰值算力

text 复制代码
GOPS = (PE 数量) × (MAC/周期) × (频率 MHz) × 2
     = 256 × 1 × 200 × 2 = 102.4 GOPS

7.2 Roofline 模型

text 复制代码
性能                  计算边界
(GOPS)      ■ 峰值算力
           ╱
          ╱  访存边界
         ╱│
        ╱ │
    ───────────────→ 运算强度 (OPs/Byte)
  • 访存受限(左)→ 增加数据复用,减少 DDR 访问
  • 计算受限(右)→ 增加 PE 数量或频率

7.3 资源利用率示例(Xilinx ZCU102)

资源 使用量 总量 利用率
LUT 185,432 274,080 67.6%
FF 243,567 548,160 44.4%
BRAM 815 912 89.4%
DSP48 2,156 2,520 85.6%

7.4 时序优化约束(参考知识库)

tcl 复制代码
create_clock -period 5.0 [get_ports sys_clk]   # 200 MHz

set_property USE_DSP48 yes [get_cells */pe_inst/mult_reg]
set_property RAM_STYLE block [get_cells */feature_buffer/buffer_A]

report_timing_summary -delay_type max -path_type full

DSP 流水线版本对比(参考《FPGA 时序优化核心技术手册》第 7 章):

版本 描述 目标 Fmax DSP 使用
v1 单拍 MACC ~150 MHz 1
v2 2 级 pipeline ~300 MHz 1
v3 完全匹配 DSP48 >600 MHz 1(纯硬核)
v4 多通道 + 复制 >600 MHz N

八、CDC 跨时钟域处理

CNN 加速器中的典型跨时钟域:DDR UI 时钟与系统时钟。

8.1 异步 FIFO(格雷码指针)

verilog 复制代码
async_fifo #(
    .DATA_WIDTH(128),
    .ADDR_WIDTH(9)
) ddr_to_sys_fifo (
    .wr_clk (ui_clk),          // DDR MIG UI 时钟 (300 MHz)
    .wr_en  (ddr_data_valid),
    .wr_data(ddr_data),

    .rd_clk (sys_clk),         // 系统时钟 (200 MHz)
    .rd_en  (fifo_read),
    .rd_data(sys_data),

    .full   (fifo_full),
    .empty  (fifo_empty)
);

8.2 配置寄存器跨域(握手 + 2FF 同步)

verilog 复制代码
// AXI-Lite 配置(慢时钟) → 数据路径(快时钟)
reg config_req;
cdc_2ff_sync ack_sync (
    .clk_dst (sys_clk),
    .data_in (config_req),
    .data_out(config_req_sync)
);

九、实战案例:ResNet-18 层映射

9.1 网络结构

text 复制代码
Input (224×224×3)
   ↓ Conv1 (7×7, s=2, 64ch)
   ↓ MaxPool (3×3, s=2)
   ↓ ResBlock1 (3×3 ×2, 64ch)  ×2
   ↓ ResBlock2 (3×3 ×2, 128ch) ×2
   ↓ ResBlock3 (3×3 ×2, 256ch) ×2
   ↓ ResBlock4 (3×3 ×2, 512ch) ×2
   ↓ GlobalAvgPool
   ↓ FC (1000 classes)

9.2 层调度伪代码

python 复制代码
for layer_id in range(num_layers):
    write_reg(LAYER_CFG, layer_params[layer_id])

    if need_reload_weight(layer_id):
        dma_load(DDR_WEIGHT_BASE, ON_CHIP_WEIGHT, weight_size)

    # Tiling 处理大特征图
    for tile_y in range(0, output_height, TILE_H):
        for tile_x in range(0, output_width, TILE_W):
            dma_load(input_tile_addr, INPUT_BUFFER, tile_size)
            write_reg(START, 1)
            while read_reg(STATUS) != DONE:
                pass
            dma_store(OUTPUT_BUFFER, output_tile_addr, tile_size)

    input_base = output_base   # 层间数据流转

9.3 资源分配估算

层类型 PE 阵列配置 BRAM 使用 周期数(估算)
Conv 7×7 16×16 800 KB 1.2M
Conv 3×3 16×16 400 KB 320K
ResBlock 时分复用 600 KB 640K
FC 向量乘法 2 MB 50K

总结

  1. 脉动阵列是 CNN 加速的核心,通过数据在 PE 间有节奏流动实现高复用、低通信
  2. 数据流动:激活值向右传递,部分和向下累加,权重每行广播
  3. 循环映射:将卷积六重循环重排展开到硬件并行维度
  4. 存储优化:多级缓存 + 乒乓 + Tiling 缓解访存瓶颈
  5. CDC 处理:DDR UI 时钟与系统时钟间用异步 FIFO / 2FF 同步
  6. 时序优化:乘法映射 DSP48 + 深流水,可将 Fmax 推到 600 MHz 以上