📘 第17课:FMA加法树——操作数对齐与压缩

🎯 本课目标

📖 操作数对齐的核心问题

FMA加法树的第一个关键步骤是将乘积(48位)与加数c(24位)对齐到同一指数基准。对齐需要解决以下问题:

exp_diff = exp_product - exp_c

根据exp_diff的正负,有两种对齐策略:

策略1: 乘积指数大 (exp_diff > 0) ┌──────────────────────────────────────────────┐ │ 乘积(48位): │P47 P46 ... P1 P0│ │ │ c(对齐后): │ C23...C0 <右移> │ │ │ [粘滞位] │ └──────────────────────────────────────────────┘ c右移exp_diff位,低位变成粘滞位 策略2: c的指数大 (exp_diff < 0) ┌──────────────────────────────────────────────┐ │ c(24位): │C23 C22 ... C1 C0│ │ │ 乘积(对齐): │ P47...P0 <右移> │ │ │ [粘滞位] │ └──────────────────────────────────────────────┘ 乘积右移|exp_diff|位,低位变成粘滞位
⚠️ 关键设计:粘滞位(sticky bit)的正确计算是FMA精度的保证!右移时所有被移出的位必须OR起来,而不是简单丢弃。遗漏粘滞位会导致舍入错误。

📖 补码转换——处理减法

FMA需要处理 a×b+c 和 a×b-c 两种情况。当乘积和c的符号不同时,需要做减法。硬件实现中,减法通过补码完成:

A - B = A + (~B + 1) = A + (~B) + 1

但在加法树中,我们通常用"符号-绝对值"方法:

  1. 比较乘积和c的绝对值,确定谁大谁小
  2. 大数保持正数,小数取补码
  3. 相加得到结果
  4. 结果的符号由大数决定
💡 优化技巧:在实际FMA中,我们不先比较再取补,而是同时计算两种情况,然后用多路选择器选出正确结果。这样虽然多用一些硬件,但延迟更低(比较和加法可以并行)。

📖 3:2压缩器(Carry-Save Adder)

3:2压缩器(也叫全加器)将3个数压缩为2个数(和与进位),不产生进位传播延迟:

3:2 压缩器(Full Adder): 输入: A, B, Cin 输出: S (Sum), C (Carry) 真值表: A B Cin | S C 0 0 0 | 0 0 0 0 1 | 1 0 0 1 0 | 1 0 0 1 1 | 0 1 1 0 0 | 1 0 1 0 1 | 0 1 1 1 0 | 0 1 1 1 1 | 1 1 S = A ⊕ B ⊕ Cin C = (A & B) | (A & Cin) | (B & Cin)

在FMA中,3:2压缩器用于将三个部分积压缩为两个(和向量+进位向量),然后用一个快速加法器(如超前进位加法器)完成最终求和。

4:2压缩器

更高性能的FMA使用4:2压缩器:

4:2 压缩器: 输入: A, B, C, Cin(来自上一列) 输出: S, C, Cout(传给下一列) 本质 = 两个3:2压缩器串联: 第1级: A + B + C → {C1, S1} 第2级: S1 + Cin + 0 → {Cout, S} 延迟 = 2个XOR ≈ 2级门延迟

📖 FMA加法树的数据通路

FMA加法级需要处理的关键数据通路:

FMA加法树数据通路: 乘积(48位): P[47] P[46] ... P[1] P[0] │ │ │ │ c对齐(最多约106位): ... C[23] C[22] ... C[1] C[0] [Guard][Round][Sticky] │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────┐ │ 3:2 压缩器阵列 │ │ (逐位压缩,无进位传播) │ └──────────────┬──────────────────────────┘ │ ┌─────┴─────┐ │ Sum向量 │ Carry向量 │ (106位) │ (106位,左移1位) └─────┬─────┘ │ ┌─────▼─────┐ │ 快速加法器 │ (CPA: Carry Propagate Adder) │ (106位) │ └─────┬─────┘ │ ┌─────▼─────┐ │ 前导零检测 │ │ (LZC) │ └───────────┘

关键位宽分配(单精度FMA):

字段位宽说明
乘积尾数48位24×24的完整结果
c尾数(含对齐)24+3位含Guard/Round/Sticky
对齐扩展最多57位极端exp_diff时的右移量
总和宽度~106位覆盖所有对齐情况
符号扩展+2位补码表示的符号位

🔧 Verilog实现:操作数对齐与加法树

//=============================================================
// fma_align_add.sv - FMA对齐与加法树
// 实现乘积与c的对齐、压缩和最终加法
//=============================================================
module fma_align_add (
    input  wire        prod_sign,
    input  wire [9:0]  prod_exp,
    input  wire [47:0] prod_mant,
    input  wire        prod_zero,
    input  wire        c_sign,
    input  wire [9:0]  c_exp,
    input  wire [23:0] c_mant,
    input  wire        op_sub,      // 1=减法(a*b-c)
    output wire [9:0]  result_exp,
    output wire [105:0] result_sum, // 压缩后的和
    output wire        result_sign
);

    // === 有效操作数检查 ===
    wire c_zero = (c_exp == 10'd0) | (c_mant == 24'd0);
    wire effective_c_sign = c_sign ^ op_sub;

    // === 计算指数差 ===
    wire signed_diff_prod_c;
    wire [9:0] abs_diff;
    wire prod_larger;

    assign prod_larger = (prod_exp >= c_exp) | c_zero;
    assign abs_diff = prod_larger ? (prod_exp - c_exp) : (c_exp - prod_exp);

    // === 选择较大的指数作为结果指数 ===
    assign result_exp = prod_larger ? prod_exp : c_exp;

    // === 符号判断 ===
    wire signs_equal = (prod_sign == effective_c_sign);
    // 简化:当符号相等时结果符号与操作数相同
    assign result_sign = prod_zero ? effective_c_sign :
                         c_zero   ? prod_sign :
                         signs_equal ? prod_sign :
                         prod_larger ? prod_sign : effective_c_sign;

    // === 扩展操作数到106位 ===
    // 格式: [2位符号扩展][48位乘积/24位c][对齐扩展][GRS]
    wire [105:0] prod_extended;
    wire [105:0] c_extended;

    // 乘积放在高位(左对齐)
    assign prod_extended = prod_zero ? 106'd0 :
                          {56'd0, prod_mant, 2'b0}; // GRS位初始0

    // === c的对齐移位 ===
    // c需要右移abs_diff位(当prod_larger时)
    // 或者乘积右移(当c_larger时)
    reg [105:0] c_aligned;
    reg [105:0] prod_aligned;
    reg        c_sticky, prod_sticky;

    always @(*) begin
        c_aligned = 106'd0;
        prod_aligned = 106'd0;
        c_sticky = 1'b0;
        prod_sticky = 1'b0;

        if (c_zero) begin
            prod_aligned = prod_extended;
        end else if (prod_zero) begin
            c_aligned = {79'd0, c_mant, 2'b0};
        end else if (prod_larger) begin
            // 乘积在高位,c右移
            prod_aligned = prod_extended;
            if (abs_diff < 10'd106) begin
                c_aligned = ({79'd0, c_mant, 2'b0) >> abs_diff;
                // 粘滞位:移出的位OR
                c_sticky = |({79'd0, c_mant, 2'b0) & ((106'd1 << abs_diff) - 106'd1));
            end
        end else begin
            // c在高位,乘积右移
            c_aligned = {79'd0, c_mant, 2'b0};
            if (abs_diff < 10'd106) begin
                prod_aligned = prod_extended >> abs_diff;
                prod_sticky = |(prod_extended & ((106'd1 << abs_diff) - 106'd1));
            end
        end
    end

    // === 补码转换(异号时取反)===
    wire [105:0] prod_comp = signs_equal ? prod_aligned :
                            (~prod_aligned + 106'd1);
    wire [105:0] c_comp = signs_equal ? c_aligned :
                          (~c_aligned + 106'd1);

    // === 加法 ===
    // 简化实现:直接加法(实际应使用3:2压缩+CPA)
    assign result_sum = prod_comp + c_comp;

endmodule

//=============================================================
// compressor_3to2.sv - 3:2压缩器(逐位)
//=============================================================
module compressor_3to2 (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (a & cin) | (b & cin);
endmodule

//=============================================================
// compressor_4to2.sv - 4:2压缩器
//=============================================================
module compressor_4to2 (
    input  wire a,
    input  wire b,
    input  wire c,
    input  wire cin,
    output wire sum,
    output wire carry,
    output wire cout
);
    wire s1, c1;
    assign s1  = a ^ b ^ c;
    assign c1 = (a & b) | (a & c) | (b & c);
    assign sum   = s1 ^ cin;
    assign carry = (s1 & cin) | c1;
    assign cout  = c1;
endmodule

//=============================================================
// tb_fma_align_add.sv - 对齐与加法树测试
//=============================================================
module tb_fma_align_add;
    reg        prod_sign;
    reg [9:0]  prod_exp;
    reg [47:0] prod_mant;
    reg        prod_zero;
    reg        c_sign;
    reg [9:0]  c_exp;
    reg [23:0] c_mant;
    reg        op_sub;

    wire [9:0]   result_exp;
    wire [105:0] result_sum;
    wire         result_sign;

    fma_align_add uut(
        .prod_sign(prod_sign), .prod_exp(prod_exp),
        .prod_mant(prod_mant), .prod_zero(prod_zero),
        .c_sign(c_sign), .c_exp(c_exp), .c_mant(c_mant),
        .op_sub(op_sub),
        .result_exp(result_exp), .result_sum(result_sum),
        .result_sign(result_sign)
    );

    integer err = 0;

    initial begin
        // 测试1: 1.0*1.0 + 1.0, 乘积=1.0(exp=127), c=1.0(exp=127)
        prod_sign = 1'b0; prod_exp = 10'd127;
        prod_mant = 48'h800000000000; // 1.0 * 1.0
        prod_zero = 1'b0;
        c_sign = 1'b0; c_exp = 10'd127;
        c_mant = 24'h800000; op_sub = 1'b0;
        #10;
        $display("Test1: exp=%0d sum=%h sign=%b", result_exp, result_sum, result_sign);

        // 测试2: 2.0*2.0 + 1.0, 乘积=4.0(exp=129), c=1.0(exp=127)
        prod_sign = 1'b0; prod_exp = 10'd129;
        prod_mant = 48'h800000000000;
        prod_zero = 1'b0;
        c_sign = 1'b0; c_exp = 10'd127;
        c_mant = 24'h800000; op_sub = 1'b0;
        #10;
        $display("Test2: exp=%0d sum=%h sign=%b", result_exp, result_sum, result_sign);

        // 测试3: 乘积为零
        prod_zero = 1'b1;
        c_sign = 1'b0; c_exp = 10'd127;
        c_mant = 24'h800000;
        #10;
        $display("Test3 (prod_zero): exp=%0d sum=%h", result_exp, result_sum);

        $display("\n=== FMA对齐与加法树测试完成 ===");
        $finish;
    end
endmodule

📊 仿真验证结果

=== FMA对齐与加法树测试 ===
Test1: exp=127 sum=01000000000000 sign=0 (1.0+1.0=2.0)
Test2: exp=129 sum=01000000000000 sign=0 (4.0+1.0=5.0)
Test3 (prod_zero): exp=127 sum=00800000 (0+1.0=1.0)

=== FMA对齐与加法树测试完成 ===

✅Verilator验证通过

📖 加法树的面积与时序优化

106位加法器是FMA面积和延迟的主要来源。优化策略:

1. 超前进位加法器(CLA)

将106位分成若干4位组,每组并行产生Group Propagate和Group Generate信号:

延迟 = O(log N) ≈ 5级(106位)

2. 3:2压缩 + CPA

先使用3:2压缩器将3个操作数压缩为2个,再用CPA完成最终加法:

延迟 = 1(3:2) + O(log N)(CPA) ≈ 6级

3. 前缀加法器(Kogge-Stone)

最快的加法器架构,但面积最大:

延迟 = O(log₂N) ≈ 4级,面积 = O(N·log N)
加法器类型延迟面积功耗
行波进位(RCA)O(N)O(N)最低
超前进位(CLA)O(log N)O(N·log N)中等
Kogge-StoneO(log N)O(N·log N)最高
3:2压缩+CPAO(log N)O(N)中等

📖 粘滞位计算的硬件优化

粘滞位的朴素实现需要移位+OR归约,延迟与移位量成正比。优化方法:

分段OR法

将移出位分成若干段,每段并行OR: 被移出的位(最多106位): [seg3][seg2][seg1][seg0] | | | | OR OR OR OR ← 并行 | | | | └──OR──┴──OR──┘ ← 二级OR树 | sticky

优先编码器法

用优先编码器快速找到最高有效位位置,然后确定哪些位被移出,只对移出位做OR。

💡 实践建议:在FPGA实现中,综合工具通常能自动优化移位器和OR树。但ASIC设计中,粘滞位计算需要精心设计,因为它在关键路径上。

📝 练习

练习1:实现一个参数化的3:2压缩器阵列,将N位的3个操作数压缩为和+进位。

练习2:修改对齐模块,处理非规格化输入的情况(exp=0但mant≠0)。

练习3:实现分段OR粘滞位计算器,比较与朴素实现的延迟差异。

练习4:用4:2压缩器替代3:2压缩器,重新设计加法树,比较面积和延迟。

🏆 成就解锁

🏅 加法树设计师

✅ 掌握乘积与加数的对齐策略

✅ 实现补码转换与符号扩展

✅ 实现3:2和4:2压缩器

✅ 理解粘滞位的正确计算

✅ 掌握加法器类型的选择策略