4. 卷积计算单元设计:乘累加器(MAC)设计、并行计算架构、流水线设计技巧

各位同学,今天我们来啃一块硬骨头——卷积计算单元。说白了,这就是整个加速器的心脏。你前面搭的DMA、缓存、控制逻辑,最终都是为了伺候好这个模块。我做了这么多年FPGA加速,可以负责任地告诉你:卷积计算单元设计得好不好,直接决定了你芯片的算力天花板在哪里。

4.1 乘累加器(MAC)——最基础的积木

先看最基础的东西。卷积运算的本质是什么?就是乘法和加法。一个3×3的卷积核,对输入特征图的一个窗口做运算,需要9次乘法和8次加法。我们把“一次乘法+一次加法”打包成一个单元,就叫MAC(Multiply-Accumulate)。

在FPGA里,MAC通常用DSP48E2来实现。Xilinx的7系列和UltraScale系列都内置了这个硬核。我个人习惯是:能用硬核就别用LUT搭乘法器。为什么?因为DSP48E2跑个500MHz轻轻松松,你用LUT搭一个16位乘法器,能跑到300MHz就算不错了。

核心要点:一个DSP48E2可以配置成一个25×18的乘法器,然后级联加法器。对于8位整数量化来说,绰绰有余。

来看一个简单的MAC代码示例:

module mac_unit #(
    parameter DATA_WIDTH = 8
)(
    input  wire clk,
    input  wire rst_n,
    input  wire [DATA_WIDTH-1:0] data_in,
    input  wire [DATA_WIDTH-1:0] weight_in,
    input  wire acc_en,          // 累加使能
    output reg  [2*DATA_WIDTH+7:0] result  // 留足累加位宽
);

    reg [2*DATA_WIDTH-1:0] mul_result;
    reg [2*DATA_WIDTH+7:0] acc;

    // 乘法阶段
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            mul_result <= 0;
        else
            mul_result <= data_in * weight_in;
    end

    // 累加阶段
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            acc <= 0;
        else if (acc_en)
            acc <= acc + mul_result;
        else
            acc <= mul_result;  // 新窗口开始,重置累加
    end

    assign result = acc;

endmodule

这段代码看起来简单,但有个坑。我曾经在一个项目里,因为acc_en的时序没处理好,导致累加结果总是差一个时钟周期。排查了整整两天才发现——嗯,这里要注意:acc_en的拉高时机必须和乘法结果对齐。

4.2 并行计算架构——让DSP转起来

单个MAC太慢了。一个3×3卷积需要9个时钟周期才能算完一个输出像素。对于1080P的图像,这得算到猴年马月去?

所以我们要做并行。怎么做?两个维度:

  • 输入通道并行:如果输入有32个通道,那就摆32个MAC,同时算
  • 输出通道并行:如果输出有64个通道,那就摆64组MAC阵列

我常用的架构是“脉动阵列”(Systolic Array)。说白了,就是让数据像流水一样在阵列里流动,每个MAC只和邻居通信。这样做的好处是:布线短、频率高、扩展性好。

个人经验:我在做YOLOv3加速器时,用了16×16的脉动阵列。每个周期可以完成256次乘累加。配合DSP48E2的级联特性,跑到了450MHz。这个频率在FPGA上已经相当可观了。

下面我画了一张图,帮你理解脉动阵列的数据流:

脉动阵列数据流示意图(4×4) 权重行0 权重行1 权重行2 权重行3 数据列0 数据列1 数据列2 数据列3 MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC MAC 图例: 特征图数据流(水平方向) 权重数据流(垂直方向) 每个MAC接收左侧传来的特征图数据和上方传来的权重数据 计算结果向下传递,部分和向右传递

你看这张图,红色箭头是特征图数据,从左往右流。绿色箭头是权重数据,从上往下流。每个MAC只做一件事:把当前的特征图数据和权重相乘,然后和左边传来的部分和相加,再传给右边的邻居。

这样做的好处是什么?你想想看,数据流是规则的,没有广播,没有复杂的交叉互联。在FPGA上,这种规则结构最容易跑出高频率。

4.3 流水线设计技巧——让每一级都忙起来

并行架构搭好了,但还有一个问题:时序。一个MAC从输入到输出,路径上有一个乘法器和一个加法器。如果数据位宽大一点,组合逻辑延迟可能超过一个时钟周期。

解决方案就是流水线。说白了,就是在关键路径上插入寄存器,把一个大组合逻辑拆成几个小段。

我一般会在三个地方插流水线:

  1. 乘法器输出后:乘法结果先寄存一拍,再送给加法器
  2. 加法器输出后:累加结果寄存一拍,再输出
  3. 数据输入处:输入的特征图和权重先寄存一拍,保证时序干净

注意:插入流水线会增加延迟(latency)。比如3级流水线,从输入到输出需要3个时钟周期。但好处是频率可以大幅提升。在卷积加速器里,我们通常不在乎几个周期的延迟,因为数据是连续流式处理的。

来看一个带流水线的MAC设计:

module pipelined_mac #(
    parameter DATA_WIDTH = 8
)(
    input  wire clk,
    input  wire rst_n,
    input  wire [DATA_WIDTH-1:0] data_in,
    input  wire [DATA_WIDTH-1:0] weight_in,
    input  wire [2*DATA_WIDTH+7:0] partial_sum_in,  // 来自左侧的部分和
    output reg  [2*DATA_WIDTH+7:0] result_out
);

    // 流水线寄存器
    reg [DATA_WIDTH-1:0] data_reg;
    reg [DATA_WIDTH-1:0] weight_reg;
    reg [2*DATA_WIDTH-1:0] mul_reg;
    reg [2*DATA_WIDTH+7:0] sum_reg;

    // 第1级:输入寄存
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            data_reg <= 0;
            weight_reg <= 0;
        end else begin
            data_reg <= data_in;
            weight_reg <= weight_in;
        end
    end

    // 第2级:乘法 + 寄存
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            mul_reg <= 0;
        else
            mul_reg <= data_reg * weight_reg;
    end

    // 第3级:加法 + 寄存
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            sum_reg <= 0;
        else
            sum_reg <= mul_reg + partial_sum_in;
    end

    assign result_out = sum_reg;

endmodule

这段代码用了3级流水线。你可能会问:为什么partial_sum_in没有寄存?嗯,这里有个讲究。在脉动阵列里,部分和是从左边MAC传过来的,它本身已经经过了一级寄存。所以不需要再额外寄存,否则会多出一拍延迟。

避坑指南:我曾经在一个项目里,为了省资源,把流水线级数从3级减到了2级。结果时序分析报了一堆violation。后来老老实实改回3级,频率从350MHz直接飙到了480MHz。所以我的建议是:在DSP路径上,宁可多插一级流水线,也别省。

4.4 资源与性能的权衡

最后聊一下资源估算。假设我们要做一个16×16的脉动阵列,每个MAC用1个DSP48E2:

参数 数值 说明
MAC数量 256 16×16阵列
DSP48E2消耗 256个 每个MAC一个DSP
LUT消耗 约2000个 控制逻辑+数据通路
BRAM消耗 约8-16个 权重缓存+行缓冲
理论算力 256 MAC/周期 每个周期完成256次乘累加
目标频率 400-500 MHz 取决于布局布线

你看这个表,256个DSP在Xilinx的KU系列上大概占30%左右的资源。剩下的资源还可以放其他模块。我个人觉得,这个规模对于大多数边缘计算场景已经够用了。

好了,卷积计算单元的核心内容就这些。记住三个关键词:MAC是基础、并行是手段、流水线是保障。把这三点吃透了,你的加速器性能就不会差。

可可精品资料库