FPGA CNN 加速器原理与实现详解
目录
- 一、核心原理
- 二、脉动阵列核心设计
- 三、数据流动的时空特性
- [四、CNN 卷积层的映射策略](#四、CNN 卷积层的映射策略)
- 五、存储层次与数据复用
- [六、完整 CNN 加速器架构](#六、完整 CNN 加速器架构)
- 七、性能评估与优化
- [八、CDC 跨时钟域处理](#八、CDC 跨时钟域处理)
- [九、实战案例:ResNet-18 层映射](#九、实战案例:ResNet-18 层映射)
一、核心原理
1.1 为什么用 FPGA 加速 CNN?
CNN(卷积神经网络)的计算特点:
- 计算密集:大量乘加运算(MAC)
- 数据复用:卷积核在特征图上滑动,输入数据被重复使用
- 并行性强:不同通道、不同卷积核可并行计算
- 访存密集:特征图和权重数据量大
FPGA 的优势:
- 定制化并行架构:可根据 CNN 层的特点设计专用硬件
- 流水线深度可控:平衡延迟与吞吐
- 低功耗:相比 GPU,相同算力下功耗更低
- 灵活性:支持不同网络结构(ResNet / MobileNet / YOLO)
二、脉动阵列(Systolic Array)核心设计
2.1 核心思想
类比心脏泵血:数据像血液一样在阵列中有节奏地流动(Systolic = 心脏收缩)。
设计目标:
- 最大化数据复用:每个数据元素在多个 PE 中被使用
- 减少全局通信:数据只在相邻 PE 间传递
- 规律的数据流:便于硬件实现和时序优化
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
关键设计点:
- 乘法映射到 DSP48:避免 LUT 实现的长延迟
- 两级流水线:乘法 1 周期,累加 1 周期
- 数据传递寄存:保证时序和同步
三、数据流动的时空特性
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 |
总结
- 脉动阵列是 CNN 加速的核心,通过数据在 PE 间有节奏流动实现高复用、低通信
- 数据流动:激活值向右传递,部分和向下累加,权重每行广播
- 循环映射:将卷积六重循环重排展开到硬件并行维度
- 存储优化:多级缓存 + 乒乓 + Tiling 缓解访存瓶颈
- CDC 处理:DDR UI 时钟与系统时钟间用异步 FIFO / 2FF 同步
- 时序优化:乘法映射 DSP48 + 深流水,可将 Fmax 推到 600 MHz 以上