大数跨境

从零设计可配置级数的 RTL 流水线:组合逻辑与多级流水的复用

从零设计可配置级数的 RTL 流水线:组合逻辑与多级流水的复用 ai算法芯片与系统
2026-10-24
2
导读:组合与流水线区别在边界用寄存器还是直连;stage 纯组合、接口不变,可复用。流水线加时钟、复位与 valid 打拍,缩短关键路径提频;组合无状态但防 latch、长路径和组合环。valid 打包进结

 

目录

  • • 📌 一、背景
  • • 📐 二、流水线深度规划
  • • ⚙️ 三、级内设计约束
  • • 🧩 四、可配置微架构的基石
  • • 🔍 五、组合与流水线的区别:以两级为例
  • • ♻️ 六、组合与流水线的复用:stage 不变
  • • 📊 七、数据验证:四种配置的真实对比
  • • 💡 八、代码复用的本质
  • • 🎯 九、总结

📌 一、背景

在数字芯片设计中,RTL 源代码的流水线深度不是随意定的。级数太少频率上不去,级数太多面积和延迟又吃不消。

更麻烦的是,同一个 RTL 设计在不同阶段往往需要不同的深度:

  • • 设计早期,功能验证优先,可能用纯组合快速迭代;
  • • 设计中期,需要平衡频率和面积,可能用 2 到 3 级;
  • • 设计后期,追求高频吞吐,可能用 8 到 10 级。

每次改深度,如果都重写 RTL,容易引入 bug,难以维护,代码也无法复用。数字芯片团队经常因此在版本之间反复修改同一段 RTL,既浪费时间,也增加验证负担。更严重的是,不同版本之间的功能不一致,往往在后期才暴露出来,排查成本极高。

🎯 本文目标:一套 RTL 代码,通过参数和边界开关,支持组合与流水线之间自由切换,并说清楚两者的区别和复用原理。


📐 二、流水线深度规划

2.1 深度估算

级数主要受四个因素约束:

因素
影响
关键路径长度
延迟越长级数越多
目标频率
频率越高级数越多
面积预算
级数越多面积越大
延迟容忍度
级数越多延迟越长

基本估算:

比如总延迟 12ns,目标周期 3.3ns,大约需要 4 级。但实际不能简单相除。每级延迟不可能完全相等,还要按逻辑块的自然边界调整。如果硬把一个完整的乘法树从中间切开,跨边界的中间信号会非常多,寄存器开销反而增大。

💡 先按逻辑块切分,再在慢的那一块内部细分。比如乘法树可拆成 booth 编码和 wallace 树两级。

2.2 逻辑块的自然边界

浮点乘法器的三个逻辑块,延迟天然不相等:

  • • S1 总延迟约 2.5ns:解包、指数相加、分类,都是小逻辑块;
  • • S2 总延迟约 6.0ns:Booth 编码、Wallace 树、找最高位、舍入,乘法树占了大部分;
  • • S2 是瓶颈:比 S1 慢一倍多,决定整条流水线周期;
  • • 跨组连接:S1 输出送 S2,S2 输出到 y;
  • • 两级不均衡:如果目标是 3ns,S2 还需要再拆,否则周期被它锁死在 6ns。

⚙️ 三、级内设计约束

3.1 级内逻辑同质

每一级应是一段边界清晰的组合逻辑:

  • • S1:解包、分类、指数相加;
  • • S2:尾数乘法、规格化、打包。

不要在一级里混入性质差异很大的逻辑,否则这一级会被最慢的子模块拖慢。比如把一个小加法器和一个大乘法树放在同一级,加法器只需要 0.5ns,乘法树需要 5ns,整级就被乘法树锁死在 5ns,加法器的资源相当于浪费了。

⚠️ 就像生产线上,前面工位几秒钟,后面工位几分钟,整条线的节拍就被后面锁死了。

3.2 级间信号少

寄存器面积等于跨级信号位宽乘深度。跨级信号越多、位宽越大,寄存器开销越大。每增加一级,就多一组全位宽的触发器,10 级流水线的寄存器面积可能是 1 级的十倍。

减少跨级信号的做法:

  • • 能早算的信号尽早算完;
  • • 特殊值标志压缩成少量位;
  • • 避免大数组跨级传递。

3.3 每级延迟均衡

这是最核心的约束。如果某一级延迟远大于其他级,整条流水线的时钟周期就被这一级锁死了。其他级再快也没用,它们每拍都有大量空闲,但空闲无法用来处理其他数据。

💡 两级不均衡时,要么拆 S2,要么把 S1 的一部分逻辑挪到 S2 前面,让两段更接近。


🧩 四、可配置微架构的基石

要把一套 RTL 变成可变深度微架构,需要四个东西:纯组合 stage、泛化边界寄存器、统一接口、编译期配置。这四样东西各司其职,缺一不可。

4.1 纯组合 stage:S1 解包


   
   
   
   
    
   
   
   
   module stage1_unpack (
    input
  logic [31:0] a,
    input
  logic [31:0] b,
    input
  logic        in_valid,
    output
 fp32_pkg::s1_t out
);
    logic
 [7:0] ea, eb;

    assign
 ea = a[30:23];
    assign
 eb = b[30:23];

    assign
 out.valid   = in_valid;
    assign
 out.sign    = a[31] ^ b[31];
    assign
 out.ma      = (ea == 0) ? {1'b0, a[22:0]} : {1'b1, a[22:0]};
    assign
 out.mb      = (eb == 0) ? {1'b0, b[22:0]} : {1'b1, b[22:0]};
    assign
 out.exp_sum = {1'b0, ea} + {1'b0, eb};

    assign
 out.a_nan   = (ea == 8'hFF) && (a[22:0] != 0);
    assign
 out.b_nan   = (eb == 8'hFF) && (b[22:0] != 0);
    assign
 out.a_inf   = (ea == 8'hFF) && (a[22:0] == 0);
    assign
 out.b_inf   = (eb == 8'hFF) && (b[22:0] == 0);
    assign
 out.a_zero  = (ea == 0)     && (a[22:0] == 0);
    assign
 out.b_zero  = (eb == 0)     && (b[22:0] == 0);
endmodule
  • • 只做解包、分类、指数相加;
  • • 没有 clk、rst_n,不依赖时钟和复位;
  • • 输出只取决于输入,是纯函数;
  • • 特殊值标志随包一起透传,下游不需要重新解析原始浮点位。

4.2 纯组合 stage:S2 乘法


   
   
   
   
    
   
   
   
   module stage2_mul (
    input
  fp32_pkg::s1_t in,
    output
 fp32_pkg::s2_t out
);
    assign
 out.valid   = in.valid;
    assign
 out.sign    = in.sign;
    assign
 out.prod    = in.ma * in.mb;
    assign
 out.exp_sum = in.exp_sum;
    assign
 out.a_nan   = in.a_nan;
    assign
 out.b_nan   = in.b_nan;
    assign
 out.a_inf   = in.a_inf;
    assign
 out.b_inf   = in.b_inf;
    assign
 out.a_zero  = in.a_zero;
    assign
 out.b_zero  = in.b_zero;
endmodule
  • • 只做乘法,不掺杂其他逻辑;
  • • 可以映射到 DSP 硬核;
  • • 上下文原样透传,不重新计算;
  • • 单独综合、单独仿真。

4.3 纯组合 stage:S3 规格化 + 舍入 + 打包


   
   
   
   
    
   
   
   
   module stage3_norm_round_pack (
    input
  fp32_pkg::s2_t in,
    output
 fp32_pkg::s3_t out
);
    // 找最高位

    logic
 [5:0] msb_idx;
    always_comb
 begin
        msb_idx = 6'd0;
        for
 (int i = 0; i < 48; i++)
            if
 (in.prod[i]) msb_idx = i[5:0];
    end


    // 规格化

    logic
 [5:0]  shamt;
    logic
 [47:0] norm;
    assign
 shamt = 6'd47 - msb_idx;
    assign
 norm  = in.prod << shamt;

    // 指数修正

    logic
 signed [11:0] exp_biased;
    assign
 exp_biased = $signed({3'b0, in.exp_sum})
                      + $signed({6'b0, msb_idx})
                      - 12'sd173;

    // RNE 舍入

    logic
 [23:0] mant24;
    assign
 mant24 = norm[47:24];

    logic
 guard, sticky, lsb;
    assign
 guard  = norm[23];
    assign
 sticky = |norm[22:0];
    assign
 lsb    = mant24[0];

    logic
 [24:0] mant_rnd;
    always_comb
 begin
        if
 (guard && (sticky || lsb))
            mant_rnd = {1'b0, mant24} + 1'b1;
        else

            mant_rnd = {1'b0, mant24};
    end


    logic
 [23:0] mant_final;
    logic
 signed [11:0] exp_final;
    always_comb
 begin
        if
 (mant_rnd[24]) begin
            mant_final = mant_rnd[24:1];
            exp_final  = exp_biased + 12'sd1;
        end
 else begin
            mant_final = mant_rnd[23:0];
            exp_final  = exp_biased;
        end

    end


    // 打包,特殊值优先

    logic
 [31:0] y_result;
    always_comb
 begin
        if
 (in.a_nan || in.b_nan)
            y_result = {in.sign, 8'hFF, 23'h400000};
        else
 if (in.a_inf || in.b_inf) begin
            if
 ((in.a_inf && in.b_zero) || (in.b_inf && in.a_zero))
                y_result = {in.sign, 8'hFF, 23'h400000};
            else

                y_result = {in.sign, 8'hFF, 23'd0};
        end

        else
 if (in.a_zero || in.b_zero)
            y_result = {in.sign, 31'd0};
        else
 if (in.prod == 48'd0)
            y_result = {in.sign, 31'd0};
        else
 if (exp_final >= 12'sd255)
            y_result = {in.sign, 8'hFF, 23'd0};
        else
 if (exp_final <= 12'sd0)
            y_result = {in.sign, 31'd0};
        else

            y_result = {in.sign, exp_final[7:0], mant_final[22:0]};
    end


    assign
 out.valid = in.valid;
    assign
 out.y     = y_result;
endmodule
  • • 只做规格化、舍入、打包;
  • • 纯组合,无状态;
  • • 输出 s3_t,只含 valid 和 y;
  • • 三个 stage 拼接后就是一个完整浮点乘法器。

4.4 泛化边界寄存器


   
   
   
   
    
   
   
   
   module pipe_reg #(
    parameter
 int W = 64
) (
    input
  logic clk, rst_n,
    input
  logic [W-1:0] din,
    output
 logic [W-1:0] dout
);
    always_ff
 @(posedge clk or negedge rst_n) begin
        if
 (!rst_n) dout <= '0;
        else
        dout <= din;
    end

endmodule
  • • 一个模块复用于所有边界;
  • • 例化时传 $bits(s1_t),自动适配位宽;
  • • 避免重复写 always_ff,减少出错;
  • • 位宽参数 W 泛化,不同边界传不同值;
  • • 例化示例:pipe_reg #(.W($bits(fp32_pkg::s1_t))) u_b1 (...);

4.5 统一接口


   
   
   
   
    
   
   
   
   typedef struct packed {
    logic
        valid;
    logic
        sign;
    logic
 [23:0] ma;
    logic
 [23:0] mb;
    logic
 [8:0]  exp_sum;
    logic
        a_nan, b_nan;
    logic
        a_inf, b_inf;
    logic
        a_zero, b_zero;
} s1_t;

typedef
 struct packed {
    logic
        valid;
    logic
        sign;
    logic
 [47:0] prod;
    logic
 [8:0]  exp_sum;
    logic
        a_nan, b_nan;
    logic
        a_inf, b_inf;
    logic
        a_zero, b_zero;
} s2_t;

typedef
 struct packed {
    logic
        valid;
    logic
 [31:0] y;
} s3_t;
  • • valid 打包进结构体,跟着数据一起走;
  • • 边界寄存器只处理一个信号,不需要区分 data 和 valid;
  • • valid 延迟自动对齐,不会和数据错位;
  • • 加字段只改 struct 定义,其他模块不动;
  • • s1_t 约 65 位,s2_t 约 65 位,s3_t 33 位。

4.6 编译期配置


   
   
   
   
    
   
   
   
   generate
    if
 (REG_EN[0]) begin : g_b1_reg
        pipe_reg #(.W($bits(fp32_pkg::s1_t))) u_b1 (
            .clk
(clk), .rst_n(rst_n),
            .din
(s1_out), .dout(b1_out)
        );
    end
 else begin : g_b1_wire
        assign
 b1_out = s1_out;
    end

endgenerate
  • • REG_EN[i] 控制第 i 个边界是否插寄存器;
  • • 1 表示插寄存器,该边界变成时序;
  • • 0 表示直连,该边界保持组合;
  • • 综合时只保留一个分支,未选中的被完全裁掉。
REG_EN
级数
延迟
2'b00
1 级(组合)
0
2'b01
2 级
1
2'b10
2 级
1
2'b11
3 级
2

🔍 五、组合与流水线的区别:以两级为例

5.1 完整数据通路对比

两级流水线和组合模式的完整结构,只在 B1 边界处不同:

  • • S1 和 S2 完全相同:两种模式用的是同一份组合逻辑,综合网表里也是同一套门;
  • • B1 是唯一区别:流水线模式是 pipe_reg,组合模式是直连;
  • • 输出寄存器都保留:保证 out_valid 和 y 对齐,延迟都是 1;
  • • s1_t 和 s2_t 接口完全相同:valid 打包在包内,边界只管寄存一个整体;
  • • 延迟差异:流水线 1 拍,组合 0 拍,正好差在 B1 这一级。

5.2 RTL 对比:只差两行


   
   
   
   
    
   
   
   
   // 两级流水线:B1 是寄存器
always_ff
 @(posedge clk or negedge rst_n) begin
    if
 (!rst_n) b1_out <= '0;
    else
        b1_out <= s1_out;
end

   
   
   
   
    
   
   
   
   // 组合模式:B1 是直连
assign
 b1_out = s1_out;
  • • 两级流水线:4 行,需要时钟和复位;
  • • 组合模式:1 行,不需要时钟和复位;
  • • 本质区别:B1有状态还是无状态;
  • • stage 模块:两种模式完全相同;
  • • 接口定义:两种模式完全相同。

5.3 一个区别,六个连锁差异

  • • 一个源头:B1 是寄存器还是连线;
  • • 六个连锁差异:由这一个区别派生;
  • • valid 无差异:因为打包在 struct 里,两种模式都不需要单独处理;
  • • 组合模式更危险:没有寄存器兜底,latch、长路径、组合环都会直接暴露;
  • • stage 不受影响:两种模式完全相同,复用不受影响。

5.4 差异一:时钟与复位

  • • 两级流水线:B1 在每个时钟沿采样,复位时清零,保证上电状态确定;
  • • 组合模式:B1 是纯连线,随时钟和复位无关,信号立即穿过;
  • • clk 端口保留:因为输出寄存器还需要时钟,顶层接口不因模式而变;
  • • rst_n 端口保留:组合模式下对 B1 无效,但对输出寄存器有效;
  • • 综合裁剪:组合模式下 B1 的 always_ff 分支被完全裁掉,不消耗触发器。

5.5 差异二:valid 对齐

  • • 两级流水线:valid 被 B1打一拍,和 data 一起延迟;
  • • 组合模式:valid直通,和 data 同一拍;
  • • 共同点:都不需要单独处理valid,因为打包在 struct 里;
  • • 打包的价值:边界是寄存器就自动打拍,是连线就自动直通,无需区分;
  • • 延迟公式:PIPE_DELAY = popcount(REG_EN),由配置自动推导。

5.6 差异三:latch 行为

  • • 两级流水线:latch 的不定态被寄存器过滤,仿真可能"看起来正常",隐患被推迟到后期;
  • • 组合模式:latch 的不定态直接传播到下游,输出错误值,问题立即暴露;
  • • 仿真风险:组合模式可能仿真和综合不一致,因为仿真器对 latch 的建模和综合不同;
  • • 防范方法:always_comb 所有分支都赋值,包括 else 和 default;
  • • 默认值:在 always_comb 开头给所有输出赋默认值,是最稳妥的写法。

5.7 差异四:关键路径

  • • 两级流水线:B1 把路径切成两段,周期取最大值 6.0ns;
  • • 组合模式:S1 和 S2 串联,周期必须 8.5ns;
  • • 频率差距:组合模式频率只有流水线的 70%,这是流水线最核心的收益;
  • • 两级不均衡:S2 比 S1 慢,所以周期由 S2 决定,S1 有空闲;
  • • 重新切分:如果 S2 太慢,可以把 S2 再拆一级,让周期降到 4ns 左右。

5.8 差异五:组合环

  • • 两级流水线:B1 寄存器切断反馈,环被打破,反馈走的是上一拍的值;
  • • 组合模式:s2_out → S1 → S2 → s2_out直接成环,同一拍内无限循环;
  • • 仿真报错:Zero-delay loop detected,仿真器检测到死循环后终止;
  • • 综合报错:组合环被综合工具拒绝,无法生成网表;
  • • 设计原则:组合模式下必须避免反馈,任何反馈都要经过寄存器。

5.9 差异汇总

项目
两级流水线
组合模式
B1
 写法
always_ff assign
时钟
需要
不需要
复位
需要
不需要
valid
自动打拍
自动直通
latch
被掩盖
直接暴露
关键路径
max(S1,S2) S1+S2
组合环
被切断
直接成环
stage
相同
相同
接口
相同
相同

5.10 结构实证:时钟接入模块数随流水级数增加

前面从逻辑和行为上分析了两者的差异。从综合/elaboration 后的连接图上,可以更直观地看到时序化的程度:流水级数越多,接入时钟信号的模块越多,组合路径被寄存器切分得越细。

0 级流水线(纯组合)

0级流水线
  • • 时钟只接入输出寄存器:组合逻辑 S1 → S2 之间没有额外寄存器;
  • • 组合路径最长:S1 和 S2 串联,关键路径为两者之和;
  • • 时序单元最少:只有输出寄存器一组触发器;
  • • 时钟负载最小:时钟树只驱动输出寄存器;
  • • 对应 REG_EN=2'b00:所有边界直连。

1 级流水线(插入 1 个边界寄存器)

1级流水线
  • • 时钟接入输出寄存器 + 1 个边界寄存器:组合路径被切成两段;
  • • 关键路径缩短:从 S1+S2 变为 max(S1, S2);
  • • 时序单元增加一组:边界寄存器接入时钟;
  • • 时钟负载增加:时钟树多驱动一组触发器;
  • • 对应 REG_EN=2'b01 或 2'b10:一个边界插寄存器。

2 级流水线(插入 2 个边界寄存器)

2级流水线
  • • 时钟接入输出寄存器 + 2 个边界寄存器:组合路径被切成三段;
  • • 关键路径进一步缩短:每段只包含一个 stage;
  • • 时序单元再增加一组:第二个边界寄存器接入时钟;
  • • 时钟负载继续增加:时钟树驱动三组触发器;
  • • 对应 REG_EN=2'b11:两个边界都插寄存器。

3 级流水线(插入 3 个边界寄存器)

3级流水线
  • • 时钟接入输出寄存器 + 3 个边界寄存器:组合路径被切成四段;
  • • 关键路径最短:每段只包含一个更细的逻辑块;
  • • 时序单元最多:三组边界寄存器全部接入时钟;
  • • 时钟负载最大:时钟树驱动四组触发器;
  • • 频率潜力最高:但面积和时钟功耗也最大。

从这四幅图可以清楚看到:每增加一级流水,就多一组边界寄存器接入时钟,组合逻辑被进一步隔离。这正是 REG_EN 位向量控制边界寄存器的物理体现,也是“级数越多、时序化程度越高、关键路径越短”的结构证据。


♻️ 六、组合与流水线的复用:stage 不变

6.1 复用的是什么

内容
是否复用
说明
S1
 模块
✅ 完全复用
纯组合,不含寄存器
S2
 模块
✅ 完全复用
纯组合,不含寄存器
S3
 模块
✅ 完全复用
纯组合,不含寄存器
s1_t
 接口
✅ 完全复用
struct packed
s2_t
 接口
✅ 完全复用
struct packed
B1
 边界
⚠️ 两种写法
always_ff
 vs assign
B2
 边界
⚠️ 两种写法
always_ff
 vs assign
输出寄存器
⚠️ 可选
保留则延迟 1

复用的核心:stage 和接口完全相同,只有边界的写法不同。 也就是说,真正随模式变化的只有边界那几行代码,其他所有 RTL 都不需要改。

6.2 为什么 stage 能复用


   
   
   
   
    
   
   
   
   module stage2_mul (
    input
  fp32_pkg::s1_t in,
    output
 fp32_pkg::s2_t out
);
    assign
 out.prod = in.ma * in.mb;
endmodule
  • • 没有 clk:不依赖时钟,任何模式都能用;
  • • 没有 rst_n:不依赖复位,无状态;
  • • 没有 always_ff:不存储状态,纯函数;
  • • 输出只取决于输入:相同的输入永远得到相同的输出;
  • • 可以任意拼接:放在哪里都一样,与上下文无关。

如果 stage 内部有 always_ff,它就被绑定成"一级流水线",无法在组合模式下使用。 这是复用失效的最常见原因。

6.3 用 generate 统一两种模式


   
   
   
   
    
   
   
   
   module fp32_mul_pipe #(
    parameter
 bit [1:0] REG_EN = 2'b11
) (
    input
  logic        clk,
    input
  logic        rst_n,
    input
  logic        in_valid,
    input
  logic [31:0] a,
    input
  logic [31:0] b,
    output
 logic        out_valid,
    output
 logic [31:0] y
);
    fp32_pkg::s1_t s1_out, b1_out;
    fp32_pkg::s2_t s2_out, b2_out;
    fp32_pkg::s3_t s3_out;

    stage1_unpack u_s1 (
        .a
(a), .b(b), .in_valid(in_valid), .out(s1_out)
    );

    generate

        if
 (REG_EN[0]) begin : g_b1_reg
            pipe_reg #(.W($bits(fp32_pkg::s1_t))) u_b1 (
                .clk
(clk), .rst_n(rst_n),
                .din
(s1_out), .dout(b1_out)
            );
        end
 else begin : g_b1_wire
            assign
 b1_out = s1_out;
        end

    endgenerate


    stage2_mul u_s2 (.in(b1_out), .out(s2_out));

    generate

        if
 (REG_EN[1]) begin : g_b2_reg
            pipe_reg #(.W($bits(fp32_pkg::s2_t))) u_b2 (
                .clk
(clk), .rst_n(rst_n),
                .din
(s2_out), .dout(b2_out)
            );
        end
 else begin : g_b2_wire
            assign
 b2_out = s2_out;
        end

    endgenerate


    stage3_norm_round_pack u_s3 (.in(b2_out), .out(s3_out));

    always_ff
 @(posedge clk or negedge rst_n) begin
        if
 (!rst_n) begin
            out_valid <= 1'b0;
            y         <= 32'd0;
        end
 else begin
            out_valid <= s3_out.valid;
            y         <= s3_out.y;
        end

    end

endmodule
  • • REG_EN[0] 控制 B1:S1 和 S2 之间;
  • • REG_EN[1] 控制 B2:S2 和 S3 之间;
  • • stage 不变:三个 stage 模块完全相同;
  • • 接口不变:s1_t、s2_t、s3_t 完全相同;
  • • 综合裁剪:未选中的分支被完全裁掉,不消耗任何资源;
  • • 延迟自动推导:PIPE_DELAY = popcount(REG_EN)。

6.4 generate 的编译期裁剪

  • • 一份源码:rtl/ 目录只有几个文件,不需要为每种模式维护一份;
  • • 两种网表:REG_EN 选择分支,综合出不同结构;
  • • 零浪费:未选中的分支被编译器完全裁掉,不留任何痕迹;
  • • generate 是编译期:不是运行时 MUX,不消耗额外面积和延迟;
  • • 配置固定:REG_EN 一旦确定,综合结果就固定,不会运行时改变。

6.5 复用的三个前提

  • • stage 纯组合:任意拼接,不绑定深度,组合和流水线共用;
  • • 接口稳定:只依赖 struct,与模式无关,改边界不改接口;
  • • valid 打包:边界自动处理,不需要单独赋值,延迟自然对齐;
  • • 三个前提缺一不可:任何一个不满足,复用就失效,就要维护多份 RTL;
  • • 收益:一份代码支持两种模式,验证和综合都只需维护一套。

6.6 如果 stage 含寄存器会怎样


   
   
   
   
    
   
   
   
   // 错误:stage 内部有寄存器
module
 stage2_mul (
    input
  logic clk,
    input
  fp32_pkg::s1_t in,
    output
 fp32_pkg::s2_t out
);
    always_ff
 @(posedge clk) out.prod <= in.ma * in.mb;
endmodule
问题
后果
组合模式无法使用
stage 自带一拍延迟
深度写死
无法自由切换
复用失效
只能用于流水线模式

stage 内部一旦有寄存器,它就变成"固定一级",无法在组合模式下工作。 即使把 REG_EN=0,这一级仍然存在,延迟无法去掉。

6.7 如果 valid 不打包会怎样


   
   
   
   
    
   
   
   
   // 错误:valid 独立传递
module
 stage2_mul (
    input
  logic in_valid,
    input
  fp32_pkg::s1_t in,
    output
 logic out_valid,
    output
 fp32_pkg::s2_t out
);
    assign
 out.prod = in.ma * in.mb;
    assign
 out_valid = in_valid;
endmodule
模式
valid 处理
两级流水线
需要单独给 valid 打拍
组合模式
需要单独 assign
边界寄存器
要处理 data 和 valid 两组信号

valid 不打包,每个边界都要单独处理 valid,改深度时要同步改多处,容易错位。 尤其是流水线有气泡或反压时,valid 和 data 一旦不同步,结果就会写到错误的位置。


📊 七、数据验证:四种配置的真实对比

7.1 TB 核心逻辑


   
   
   
   
    
   
   
   
   #include "Vfp32_mul_pipe.h"
#include "verilated.h"

#include <cstdio>

#include <cstring>

#include <cstdint>

#include <random>

#include <vector>


#ifndef PIPE_DELAY

#error "PIPE_DELAY must be defined by CMake"

#endif


static uint32_t rand_fp32(std::mt19937 &rng) 
{
    uint32_t
 sign = rng() & 1;
    uint32_t
 exp  = 100 + (rng() % 55);   // 避免溢出和下溢
    uint32_t
 mant = rng() & 0x7FFFFF;
    return
 (sign << 31) | (exp << 23) | mant;
}

static uint32_t sw_mul(uint32_t a, uint32_t b) 
{
    float
 fa, fb;
    std::memcpy(&fa, &a, 4);
    std::memcpy(&fb, &b, 4);
    float
 fc = fa * fb;
    uint32_t
 c;
    std::memcpy(&c, &fc, 4);
    return
 c;
}

int main(int argc, char **argv) 
{
    Verilated::commandArgs(argc, argv);
    auto
 *top = new Vfp32_mul_pipe;

    std::mt19937 rng(20260225)
;
    const
 int N = 200;

    std::vector<uint32_t> qa(N), qb(N), exp_y(N)
;
    for
 (int i = 0; i < N; i++) {
        qa[i]    = rand_fp32(rng);
        qb[i]    = rand_fp32(rng);
        exp_y[i] = sw_mul(qa[i], qb[i]);
    }

    // 复位

    top->clk = 0; top->rst_n = 0;
    top->in_valid = 0; top->a = 0; top->b = 0;
    for
 (int i = 0; i < 4; i++) {
        top->clk = 0; top->eval();
        top->clk = 1; top->eval();
    }
    top->rst_n = 1;

    int
 errors = 0;
    int
 valid_checked = 0;
    int
 out_count = 0;
    int
 first_out_cycle = -1;
    int
 last_out_cycle  = -1;

    const
 int max_cycles = N + PIPE_DELAY + 20;
    int
 cycle = 0;

    for
 (; cycle < max_cycles; cycle++) {
        top->clk = 0;
        if
 (cycle < N) {
            top->in_valid = 1;
            top->a = qa[cycle];
            top->b = qb[cycle];
        } else {
            top->in_valid = 0;
            top->a = 0;
            top->b = 0;
        }
        top->eval();
        top->clk = 1;
        top->eval();

        int
 out_idx = cycle - PIPE_DELAY;
        if
 (out_idx >= 0 && out_idx < N) {
            valid_checked++;
            if
 (top->out_valid != 1) {
                printf
("[ERR] cycle=%d idx=%d out_valid=%d\n",
                       cycle, out_idx, (int)top->out_valid);
                errors++;
            } else if ((uint32_t)top->y != exp_y[out_idx]) {
                if
 (errors < 10) {
                    printf
("[ERR] cycle=%d idx=%d got=%08x exp=%08x\n",
                           cycle, out_idx,
                           (uint32_t)top->y, exp_y[out_idx]);
                }
                errors++;
            }
        }

        if
 (top->out_valid) {
            out_count++;
            if
 (first_out_cycle < 0) first_out_cycle = cycle;
            last_out_cycle = cycle;
        }

        if
 (out_count == N) break;
    }

    int
 total_cycles = cycle + 1;
    int
 fill_cycles  = first_out_cycle + 1;
    int
 drain_cycles = total_cycles - (last_out_cycle + 1);
    int
 steady       = total_cycles - fill_cycles - drain_cycles;

    printf
("=== PIPE_DELAY=%d ===\n", PIPE_DELAY);
    printf
("N=%d, Checked=%d, Errors=%d\n", N, valid_checked, errors);
    printf
("latency        : %d\n", PIPE_DELAY);
    printf
("fill_cycles    : %d\n", fill_cycles);
    printf
("steady_cycles  : %d\n", steady);
    printf
("drain_cycles   : %d\n", drain_cycles);
    printf
("total_cycles   : %d\n", total_cycles);
    printf
("throughput     : %.3f result/cycle\n",
           (double)out_count / total_cycles);

    top->final();
    delete
 top;
    return
 errors ? 1 : 0;
}
  • • PIPE_DELAY 由构建系统传入:不写死,换配置不用改 TB;
  • • 随机正常浮点数:避免溢出和下溢,专注验证乘法通路;
  • • 与软件 float 逐拍对比:任何一拍出错都会被捕获;
  • • 跑到最后一个输出就停:总拍数等于实际运行拍数;
  • • 统计 fill、steady、drain:完整反映流水线的填充和排空;
  • • 四种配置共用同一套 TB:只改 PIPE_DELAY,TB 代码完全不变。

7.2 四种配置的真实输出


   
   
   
   
    
   
   
   
   === PIPE_DELAY=0(1 级)===
N=200, Checked=200, Errors=0
latency        : 0
fill_cycles    : 1
steady_cycles  : 199
drain_cycles   : 0
total_cycles   : 200
throughput     : 1.000 result/cycle

=== PIPE_DELAY=1(2 级)===
N=200, Checked=200, Errors=0
latency        : 1
fill_cycles    : 2
steady_cycles  : 199
drain_cycles   : 0
total_cycles   : 201
throughput     : 0.995 result/cycle

=== PIPE_DELAY=2(3 级)===
N=200, Checked=200, Errors=0
latency        : 2
fill_cycles    : 3
steady_cycles  : 199
drain_cycles   : 0
total_cycles   : 202
throughput     : 0.990 result/cycle

7.3 数据说明什么

REG_EN latency fill total throughput
0
0
1
200
1.000
1
1
2
201
0.995
3
2
3
202
0.990

公式精确吻合:

7.4 吞吐随 N 的摊薄曲线

  • • N 小时级数差异显著:填充开销占比大,3 级吞吐只有 1 级的 1/3;
  • • N 大时趋近 1:填充被摊薄,3 级吞吐也接近 1.000;
  • • 级数越多吞吐越低:每周期整体吞吐递减,因为填充开销被摊到总拍数里;
  • • 频率才是级数的真正收益:但仿真器不模拟门延迟,无法直接给出频率数据;
  • • 流式场景:N 远大于 latency 时,所有配置吞吐都趋近 1,级数差异可忽略。

💡 八、代码复用的本质

8.1 把"变"和"不变"分离

维度
是否变化
处理方式
计算逻辑
不变
纯组合模块,只写一次
边界位置
变
REG_EN
 编译期选择
接口形状
不变
struct packed
 打包
延迟
变
由 REG_EN 推导

复用的本质:把不随配置变化的部分固定下来,把变化的部分参数化。 这样只需要维护一份计算逻辑,所有配置都从这一份派生。

8.2 三个层次的复用

  • • 不变的三个东西:计算、寄存器、接口;
  • • 变化的三个东西:深度、延迟、模式;
  • • 复用 = 固定不变的,参数化变化的;
  • • stage 不受模式影响:组合和流水线共用同一份,不需要维护两套;
  • • 边界是唯一变化点:always_ff 或 assign,只有这一行随模式变。

8.3 三个前提条件

前提
说明
模块边界清晰
stage 只做一件事
状态与逻辑分离
valid 打包进 struct
变化点在编译期
REG_EN
 是常量

🎯 九、总结

问题
答案
深度怎么定
总延迟除目标周期
每级要求
同质、信号少、延迟均衡
可配置微架构需要什么
stage、pipe_reg、struct、REG_EN
组合与流水的区别 边界 assign vs always_ff
一个区别引出几个差异
六个连锁差异
组合模式要注意什么
latch、关键路径、组合环
stage 能复用吗 ✅ 两种模式完全相同
接口能复用吗
✅ 完全相同
valid 怎么对齐
打包进 struct,自动对齐
四种配置功能一致吗
✅ Errors=0
级数越多吞吐越高吗
❌ 每周期吞吐递减
代码复用的本质
把计算逻辑和流水线深度分离

🎯 组合和流水线的唯一区别,是边界 B1 用 assign 还是 always_ff。但一个区别引出六个连锁差异:时钟、复位、valid、latch、关键路径、组合环。

组合模式不需要时钟和复位,但必须重点防范 latch、长路径和组合环,因为没有寄存器兜底。两级流水线把 S1 和 S2 分开,关键路径取 max(S1, S2);组合模式把 S1 和 S2 串联,关键路径是 S1 + S2,频率只有流水线的 70%。

从结构连接图可以直观看到:流水级数越多,接入时钟信号的时序模块越多,组合路径被寄存器切分得越细,关键路径越短,时序化程度越高。

valid 打包进 struct 是复用的关键。它让边界寄存器只处理一个信号,valid 跟着数据走,延迟自动对齐,两种模式都不需要单独处理 valid。

stage 和接口在两种模式下完全相同,唯一变化的是边界。这就是代码复用的本质:把不随配置变化的部分固定,把变化的部分参数化。三个纯组合 stage、一个泛化 pipe_reg、一个统一 struct、一个 REG_EN 开关,四样东西组合起来,就实现了组合和流水线之间的自由切换,而计算逻辑只写了一次。

四种配置的仿真结果完全一致,Errors=0。total = N + latency、fill = latency + 1、throughput = N / (N + latency) 精确吻合。小 N 时级数差异显著,大 N 时趋近 1,填充开销被摊薄。

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 238
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读6.8k
粉丝0
内容238