4. MAC阵列设计(一):乘累加单元(MAC)的RTL实现、流水线设计、输入重定时
各位同学,今天咱们来啃一块硬骨头——MAC单元。说它硬,是因为整个NPU的计算能力,说白了就靠它撑着。你想想看,卷积、全连接、矩阵乘法,这些深度学习里的核心操作,拆到最底层,全是一堆乘法和加法在反复折腾。
我个人习惯把MAC单元比作NPU的「心脏」。心脏跳得快不快,直接决定了整个芯片的性能。所以这一章,咱们就把这颗心脏的RTL实现、流水线怎么搭、输入重定时怎么玩,彻底讲透。
4.1 从数学到硬件:MAC单元的本质
先看数学公式,很简单:
Y = A × B + C
一个乘法,一个加法,完事。但到了硬件里,事情就没这么简单了。我在项目中遇到过最典型的问题:新手直接写一个组合逻辑的乘法器,然后发现时序根本跑不通。为什么?因为乘法器在硅片上是真真切切要花时间的。
咱们来看一个最基础的MAC RTL实现:
module mac_unit #(
parameter DATA_WIDTH = 8
)(
input logic clk,
input logic rst_n,
input logic valid_in,
input logic [DATA_WIDTH-1:0] a,
input logic [DATA_WIDTH-1:0] b,
input logic [2*DATA_WIDTH-1:0] c,
output logic [2*DATA_WIDTH-1:0] result,
output logic valid_out
);
logic [2*DATA_WIDTH-1:0] mul_result;
logic [2*DATA_WIDTH-1:0] acc_result;
// 乘法
assign mul_result = a * b;
// 累加
assign acc_result = mul_result + c;
// 输出寄存器
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
result <= '0;
valid_out <= 1'b0;
end else begin
result <= acc_result;
valid_out <= valid_in;
end
end
endmodule
嗯,这里要注意:上面的代码虽然功能正确,但实际项目中几乎没人这么用。为什么?因为乘法器和加法器串在一起,组合逻辑路径太长了。你想想看,8位乘法还好,要是32位、64位呢?那延迟直接爆炸。
4.2 流水线设计:把长路径切短
流水线的核心思想很简单:把一个大任务拆成几个小任务,每个小任务用一个时钟周期完成。这样虽然单个数据的延迟增加了,但整体吞吐率上去了。
对于MAC单元,我建议至少拆成两级流水线:
- 第一级:乘法运算 + 结果寄存器
- 第二级:加法运算 + 结果寄存器
来看改进后的代码:
module mac_pipeline #(
parameter DATA_WIDTH = 8
)(
input logic clk,
input logic rst_n,
input logic valid_in,
input logic [DATA_WIDTH-1:0] a,
input logic [DATA_WIDTH-1:0] b,
input logic [2*DATA_WIDTH-1:0] c,
output logic [2*DATA_WIDTH-1:0] result,
output logic valid_out
);
// 流水线寄存器 - 第一级
logic [2*DATA_WIDTH-1:0] mul_reg;
logic [2*DATA_WIDTH-1:0] c_reg;
logic valid_stage1;
// 流水线寄存器 - 第二级
logic [2*DATA_WIDTH-1:0] acc_reg;
logic valid_stage2;
// 第一级:乘法
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mul_reg <= '0;
c_reg <= '0;
valid_stage1 <= 1'b0;
end else begin
mul_reg <= a * b;
c_reg <= c;
valid_stage1 <= valid_in;
end
end
// 第二级:加法
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
acc_reg <= '0;
valid_stage2 <= 1'b0;
end else begin
acc_reg <= mul_reg + c_reg;
valid_stage2 <= valid_stage1;
end
end
// 输出
assign result = acc_reg;
assign valid_out = valid_stage2;
endmodule
4.3 输入重定时:让时序再飞一会儿
好了,流水线加上了,时序应该没问题了吧?不一定。你想想看,如果输入数据a和b是从很远的地方送过来的,走线延迟本身就很大。这时候,即使MAC内部时序OK,输入路径也可能成为瓶颈。
怎么办?输入重定时(Input Retiming)。说白了,就是在输入端再加一级寄存器,把输入数据先锁存一拍。
我个人习惯的做法是:
- 先看综合报告,找到关键路径
- 如果关键路径在输入端口到乘法器之间,就在输入加寄存器
- 如果关键路径在乘法器到加法器之间,就在中间加流水线
- 如果关键路径在加法器到输出之间,就在输出加寄存器
来看加入输入重定时后的完整代码:
module mac_retimed #(
parameter DATA_WIDTH = 8
)(
input logic clk,
input logic rst_n,
input logic valid_in,
input logic [DATA_WIDTH-1:0] a,
input logic [DATA_WIDTH-1:0] b,
input logic [2*DATA_WIDTH-1:0] c,
output logic [2*DATA_WIDTH-1:0] result,
output logic valid_out
);
// 输入重定时寄存器
logic [DATA_WIDTH-1:0] a_reg, b_reg;
logic [2*DATA_WIDTH-1:0] c_reg_in;
logic valid_in_reg;
// 流水线寄存器 - 第一级
logic [2*DATA_WIDTH-1:0] mul_reg;
logic [2*DATA_WIDTH-1:0] c_reg_pipe;
logic valid_stage1;
// 流水线寄存器 - 第二级
logic [2*DATA_WIDTH-1:0] acc_reg;
logic valid_stage2;
// 输入重定时
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
a_reg <= '0;
b_reg <= '0;
c_reg_in <= '0;
valid_in_reg <= 1'b0;
end else begin
a_reg <= a;
b_reg <= b;
c_reg_in <= c;
valid_in_reg <= valid_in;
end
end
// 第一级:乘法
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mul_reg <= '0;
c_reg_pipe <= '0;
valid_stage1 <= 1'b0;
end else begin
mul_reg <= a_reg * b_reg;
c_reg_pipe <= c_reg_in;
valid_stage1 <= valid_in_reg;
end
end
// 第二级:加法
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
acc_reg <= '0;
valid_stage2 <= 1'b0;
end else begin
acc_reg <= mul_reg + c_reg_pipe;
valid_stage2 <= valid_stage1;
end
end
// 输出
assign result = acc_reg;
assign valid_out = valid_stage2;
endmodule
4.4 流水线深度怎么选?
这个问题没有标准答案。我一般遵循几个原则:
| 数据位宽 | 目标频率 | 建议流水线级数 | 备注 |
|---|---|---|---|
| 8位 | < 500MHz | 1-2级 | 组合逻辑即可,加1级保险 |
| 16位 | 500MHz - 1GHz | 2-3级 | 乘法器需要拆开 |
| 32位 | > 1GHz | 3-5级 | 可能需要专用乘法器IP |
当然,这只是经验值。具体用几级,还是要看综合后的时序报告。我记得有一次,一个16位MAC我用了3级流水线,结果综合后时序还有余量,我就试着减了一级,发现也能跑。所以,别死板,多试试。
4.5 本章小结
这一章咱们把MAC单元从零开始,一步步搭了起来。核心就三件事:
- 基础实现:乘法+加法,但别用纯组合逻辑
- 流水线设计:把长路径切短,提升时序
- 输入重定时:解决输入路径的时序瓶颈
下一章,咱们要把这些MAC单元拼成阵列,真正开始搭建NPU的计算核心。到时候你会发现,单个MAC的时序做好了,阵列的时序问题会少很多。所以,这一章的基础一定要打牢。
好,今天就到这里。有问题随时问我。
公众号:蓝海资料掘金营,微信deep3321