4. 乘加器(MAC)微架构设计:基本MAC单元、流水线MAC、低功耗MAC设计
乘加器,也就是MAC,是卷积加速器的心脏。说白了,卷积运算就是不停地做“乘”和“加”。一个卷积层里,可能有几百万甚至上亿次这样的操作。所以,MAC单元设计得好不好,直接决定了加速器的性能和功耗。
我个人习惯把MAC设计分成三个层次来看:先搞定最基本的单元,再考虑怎么让它跑得更快,最后琢磨怎么让它更省电。咱们一个一个来聊。
4.1 基本MAC单元:从零开始搭积木
最基本的MAC单元,干的事情很简单:累加值 = 累加值 + 输入A * 输入B。嗯,就这一句话。但在硬件里,它对应着一个乘法器和一个加法器。
结构长什么样?
一个典型的单周期MAC,数据流是这样的:
- 输入寄存器:先把输入A和输入B锁存住,避免组合逻辑的毛刺传到内部。
- 乘法器:做A*B的运算。我建议用布斯编码的乘法器,面积和速度比较均衡。
- 加法器:把乘法结果和上一次的累加值相加。
- 累加寄存器:存着当前的累加结果,同时反馈给加法器的另一个输入端。
这里有个坑,我刚开始做的时候踩过。累加寄存器的清零信号,一定要处理好。卷积计算每个窗口开始前,累加值必须归零。我曾经用了一个异步清零,结果时序分析老报错。后来改成同步清零,配合数据有效信号,问题就解决了。
核心要点:基本MAC的位宽选择很关键。输入数据是8bit,权重是8bit,乘法结果就是16bit。累加过程中,为了防止溢出,累加器位宽一般要扩展到32bit甚至更宽。具体多宽?得看卷积窗口大小和输入通道数。
// 一个简单的单周期MAC Verilog代码
module mac_single_cycle (
input clk,
input rst_n,
input [7:0] data_in,
input [7:0] weight,
input clear, // 累加清零信号
output reg [31:0] acc_out
);
wire [15:0] mult_result;
wire [31:0] add_result;
// 乘法
assign mult_result = data_in * weight;
// 加法 + 累加
assign add_result = acc_out + mult_result;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
acc_out <= 32'd0;
else if (clear)
acc_out <= 32'd0;
else
acc_out <= add_result;
end
endmodule
你看,代码很简单。但实际项目里,我不会这么写。为什么?因为单周期MAC的时序压力太大了。乘法器加加法器,组合逻辑路径太长,频率根本上不去。这就引出了流水线MAC。
4.2 流水线MAC:让时钟跑得更快
单周期MAC的问题,说白了就是“一口吃不成胖子”。我们把乘法和加法拆开,中间插一级寄存器,这就是流水线。
两级流水线MAC
我一般这么切:
- 第一级:输入寄存 + 乘法运算。这一级结束,乘法结果被锁存到中间寄存器。
- 第二级:加法运算 + 累加寄存。这一级把乘法结果和上一次的累加值相加,然后存回去。
这样做的好处很明显:每一级的组合逻辑变短了,时钟频率可以提上去。代价是,结果会晚一个时钟周期出来。不过对于卷积加速器来说,这点延迟完全能接受。
我的经验:流水线级数不是越多越好。对于8bit x 8bit的乘法,两级流水线足够了。如果数据位宽更大,比如16bit x 16bit,我可能会用到三级:第一级做部分积生成,第二级做部分积压缩,第三级做最终加法。
// 两级流水线MAC
module mac_pipeline (
input clk,
input rst_n,
input [7:0] data_in,
input [7:0] weight,
input clear,
output reg [31:0] acc_out
);
reg [15:0] mult_reg; // 中间寄存器
wire [15:0] mult_result;
wire [31:0] add_result;
// 第一级:乘法
assign mult_result = data_in * weight;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
mult_reg <= 16'd0;
else
mult_reg <= mult_result;
end
// 第二级:加法 + 累加
assign add_result = acc_out + mult_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
acc_out <= 32'd0;
else if (clear)
acc_out <= 32'd0;
else
acc_out <= add_result;
end
endmodule
你想想看,流水线MAC其实就是用寄存器换频率。面积大了点,但性能提升明显。在卷积加速器里,我们通常会例化几十上百个这样的MAC单元,组成一个脉动阵列。
4.3 低功耗MAC设计:省下来的电就是性能
做芯片的都知道,功耗是大事。尤其是卷积加速器,MAC阵列一跑起来,功耗蹭蹭往上涨。我做过一个项目,芯片回来后一测,MAC阵列功耗占了总功耗的60%以上。从那以后,我对低功耗设计就特别上心。
策略一:操作数隔离
这是最常用的一招。当MAC单元不工作时,把输入数据锁住,不让它们翻转到乘法器里。说白了,就是加一个使能信号。
// 带操作数隔离的MAC
module mac_low_power (
input clk,
input rst_n,
input [7:0] data_in,
input [7:0] weight,
input en, // 使能信号
input clear,
output reg [31:0] acc_out
);
reg [7:0] data_reg, weight_reg;
wire [15:0] mult_result;
wire [31:0] add_result;
// 操作数隔离:只有en有效时才更新输入寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_reg <= 8'd0;
weight_reg <= 8'd0;
end else if (en) begin
data_reg <= data_in;
weight_reg <= weight_in;
end
// 注意:en无效时,data_reg和weight_reg保持不变
end
assign mult_result = data_reg * weight_reg;
// ... 后续累加逻辑同前
endmodule
策略二:门控时钟
这个更狠。当MAC单元空闲时,直接把时钟关掉。不过要注意,门控时钟对后端实现不太友好,容易引入时钟偏斜。我建议只在粗粒度上使用,比如整个MAC阵列的时钟门控,而不是每个MAC单独门控。
策略三:数据格式优化
嗯,这个是我在项目中摸索出来的。如果输入数据有很多0,比如经过ReLU激活函数后,一半以上的数据都是0。这时候,我们可以加一个“零值检测”电路。检测到输入为0时,直接跳过乘法运算,累加值保持不变。
注意:零值检测虽然省功耗,但会引入额外的比较器逻辑。如果数据中0的比例不高,反而得不偿失。我一般会在仿真阶段统计一下数据的稀疏度,再决定要不要加这个功能。
4.4 三种MAC的对比
我把这三种MAC的特点整理了一下,方便你对比选择:
| 类型 | 面积 | 频率 | 功耗 | 适用场景 |
|---|---|---|---|---|
| 基本MAC | 小 | 低 | 中 | 低频、小规模设计 |
| 流水线MAC | 中 | 高 | 中高 | 高性能卷积加速器 |
| 低功耗MAC | 中 | 中 | 低 | 移动端、边缘计算 |
实际项目中,我通常会把流水线MAC和低功耗技术结合起来用。比如,在流水线MAC的基础上,加上操作数隔离和门控时钟。这样既保证了性能,又把功耗控制在合理范围内。
一句话总结:MAC设计没有银弹。你得根据项目的性能目标和功耗预算,在面积、速度、功耗之间做权衡。我个人建议,先从流水线MAC入手,再根据功耗仿真结果,逐步加入低功耗特性。
好了,关于MAC微架构设计,我就聊这么多。从基本单元到流水线,再到低功耗技巧,每一步都有它的道理。你在实际项目中,不妨先从最简单的开始,然后根据仿真结果一步步优化。记住,没有最好的设计,只有最合适的设计。