📘 第19课:FMA精度优化——双舍入与subnormal处理

🎯 本课目标

📖 双舍入问题

双舍入是FMA实现中最隐蔽的精度陷阱。当中间结果恰好处于两个可表示数中间时,两次舍入可能产生错误:

双舍入问题示例: 假设3位精度,RNE舍入: 精确值 = 1.011100... (需要舍入到3位) 第1次舍入到4位: 1.011|1 → 1.100 (RNE: 1进位因为后缀100 > 100) 第2次舍入到3位: 1.10|0 → 1.10 (RNE: 0不进位) 但直接舍入到3位: 1.01|1100 → 1.10 (RNE: 1100 > 100, 进位) 结果一致! 但另一个例子: 精确值 = 1.011011... (需要舍入到3位) 第1次舍入到4位: 1.011|0 → 1.011 (RNE: 0不进位) 第2次舍入到3位: 1.01|1 → 1.10 (RNE: 1进位,因为后缀=1000是中间) 但直接舍入到3位: 1.01|1011 → 1.10 (RNE: 1011 > 100, 进位) 结果也一致! 问题出在: 精确值 = 1.0111... 第1次到4位: 1.01|11 → 1.100 (进位) 第2次到3位: 1.10|0 → 1.10 (不进位) 直接到3位: 1.01|11 → 1.10 (进位) 这个例子也OK... 实际陷阱出现在扩展精度中间格式!

更准确地说,双舍入问题出现在:

⚠️ FMA的核心承诺:整个FMA操作只有一次舍入。任何中间舍入都违反了IEEE 754对FMA的定义。我们的实现通过保留完整48位乘积并在最终才舍入来保证这一点。

📖 非规格化数(Subnormal)处理

FMA中的非规格化数处理是最复杂的边界情况:

输入非规格化数

当a、b或c是非规格化数时,隐含位为0而不是1:

规格化数: mantissa = 1.xxx...x, 实际指数 = E - 127
非规格化数: mantissa = 0.xxx...x, 实际指数 = 1 - 127 = -126

输出非规格化数

当FMA结果太小无法用规格化数表示时,需要输出非规格化数:

非规格化输出的判断: 规格化结果指数 = exp_result - bias 如果 exp_result <= 0: 结果需要右移 |exp_result| 位变成非规格化数 尾数 = 0.xxx...x (不再有隐含的1) 指数字段 = 0 渐近下溢过程: 1.111...1 × 2^(-126) ← 最小规格化数 1.011...1 × 2^(-126) 0.111...1 × 2^(-126) ← 最大非规格化数 0.011...1 × 2^(-126) ... 0.000...1 × 2^(-126) = 2^(-149) ← 最小非规格化数
💡 实现策略:在规格化阶段,如果调整后的指数小于1(单精度下溢阈值),则需要特殊处理:不左移到隐含1位置,而是根据指数差确定右移量,生成非规格化结果。

📖 前导零预测器(LZA)

前导零计数器(LZC)在加法完成后才能工作,这在关键路径上。前导零预测器(LZA)可以与加法并行计算:

LZC vs LZA 时序对比: LZC方案: 加法 ──→ LZC ──→ 移位 ──→ 舍入 |←── 串行 ──→| LZA方案: ┌── 加法 ──→ 验证/修正 ──→ 移位 ──→ 舍入 │ └── LZA ──→ (预测值) ──→│ (与加法并行) LZA节省1级延迟!

LZA算法原理

LZA基于对加法器输入的观察来预测前导零数量:

对于 A + B (A和B为正数): 预测LZ ≈ f(A, B) = 第一个 A⊕B=0 且 A·B=0 的位置之前

LZA可能高估1位(预测多了1个前导零),需要修正:

LZA预测修正: 预测: 3个前导零 实际: 2个前导零 (加法产生了进位) 修正: 检查预测位置的进位 如果有进位 → 实际LZ = 预测LZ - 1 如果无进位 → 实际LZ = 预测LZ

📖 精确粘滞位计算

粘滞位的精确性直接影响舍入正确性。在FMA中,粘滞位有三个来源:

粘滞位来源: 1. 乘积移出的低位 (乘法级的sticky) 2. c移出的低位 (对齐级的sticky) 3. 规格化移出的低位 (规格化级的sticky) 最终sticky = sticky_prod | sticky_c | sticky_norm 其中: sticky_prod: 乘积右移时移出的位 (通常为0,因为乘积不右移) sticky_c: c对齐右移时移出的位 sticky_norm: 规格化右移时移出的位

粘滞位优化:分段OR

对于可能被移出的宽位(如c对齐时可能右移多达57位),使用分段OR:

// 分段OR粘滞位计算
wire sticky_seg0 = |c_mant_shifted[5:0];
wire sticky_seg1 = |c_mant_shifted[11:6];
wire sticky_seg2 = |c_mant_shifted[17:12];
// ... 更多段
wire sticky_final = sticky_seg0 | sticky_seg1 | sticky_seg2 | ...;

🔧 Verilog实现:精度优化FMA

下面实现包含subnormal处理和精确舍入的FMA:

//=============================================================
// fma_precision.sv - FMA精度优化版本
// 包含: subnormal处理, 精确粘滞位, 多舍入模式
//=============================================================
module fma_precision (
    input  wire [31:0] a,
    input  wire [31:0] b,
    input  wire [31:0] c,
    input  wire [1:0]  rm,    // 舍入模式: 00=RNE,01=RZ,10=RP,11=RN
    output wire [31:0] result
);

    // ========== 解码 ==========
    wire sign_a = a[31], sign_b = b[31], sign_c = c[31];
    wire [7:0] exp_a = a[30:23], exp_b = b[30:23], exp_c = c[30:23];
    wire [22:0] frac_a = a[22:0], frac_b = b[22:0], frac_c = c[22:0];
    wire a_zero=(exp_a==8'b0)&(frac_a==23'b0);
    wire b_zero=(exp_b==8'b0)&(frac_b==23'b0);
    wire c_zero=(exp_c==8'b0)&(frac_c==23'b0);
    wire a_nan=(exp_a==8'hFF)&(frac_a!=23'b0);
    wire b_nan=(exp_b==8'hFF)&(frac_b!=23'b0);
    wire c_nan=(exp_c==8'hFF)&(frac_c!=23'b0);
    wire a_inf=(exp_a==8'hFF)&(frac_a==23'b0);
    wire b_inf=(exp_b==8'hFF)&(frac_b==23'b0);
    wire c_inf=(exp_c==8'hFF)&(frac_c==23'b0);
    wire a_denorm=(exp_a==8'b0)&(frac_a!=23'b0);
    wire b_denorm=(exp_b==8'b0)&(frac_b!=23'b0);
    wire c_denorm=(exp_c==8'b0)&(frac_c!=23'b0);

    // 处理非规格化输入: 计算前导零用于规格化
    wire [4:0] lzc_a, lzc_b, lzc_c;
    // 简化: 假设LZC已正确计算前导零数
    assign lzc_a = 5'd0; // placeholder
    assign lzc_b = 5'd0;
    assign lzc_c = 5'd0;

    // 尾数(含隐含位) 和 扩展指数
    wire [23:0] mant_a = (exp_a==8'b0) ? {1'b0,frac_a} : {1'b1,frac_a};
    wire [23:0] mant_b = (exp_b==8'b0) ? {1'b0,frac_b} : {1'b1,frac_b};
    wire [23:0] mant_c = (exp_c==8'b0) ? {1'b0,frac_c} : {1'b1,frac_c};
    wire [9:0] exp_a_e = (exp_a==8'b0) ? 10'd1 : {2'b0,exp_a};
    wire [9:0] exp_b_e = (exp_b==8'b0) ? 10'd1 : {2'b0,exp_b};
    wire [9:0] exp_c_e = (exp_c==8'b0) ? 10'd1 : {2'b0,exp_c};

    // ========== 特殊值 ==========
    wire any_nan = a_nan|b_nan|c_nan;
    wire prod_inf = a_inf|b_inf;
    wire inf_zero = (a_inf&b_zero)|(a_zero&b_inf);
    wire inf_inf_sub = prod_inf&c_inf&(sign_a^sign_b^sign_c);
    wire is_nan = any_nan|inf_zero|inf_inf_sub;
    wire is_inf = (prod_inf|c_inf)&~is_nan;
    wire inf_sign_out = prod_inf ? (sign_a^sign_b) : sign_c;

    // ========== 乘法 ==========
    wire p_sign = sign_a ^ sign_b;
    wire [47:0] product = mant_a * mant_b;
    wire [9:0] p_exp = exp_a_e + exp_b_e - 10'd127;
    wire p_need_sh = ~product[47];
    wire [47:0] p_mant = p_need_sh ? {product[46:0],1'b0} : product;
    wire [9:0] p_exp_n = p_need_sh ? (p_exp - 10'd1) : p_exp;

    // ========== 对齐 ==========
    wire p_larger = (p_exp_n >= exp_c_e) | c_zero;
    wire [9:0] diff = p_larger ? (p_exp_n - exp_c_e) : (exp_c_e - p_exp_n);
    wire [9:0] al_exp = p_larger ? p_exp_n : exp_c_e;

    // 扩展位宽: 48(乘积) + 3(GRS) + 额外对齐空间 = 56位
    wire [55:0] p_ext = {p_mant, 8'b0}; // 乘积在高位
    wire [55:0] c_ext = {32'd0, mant_c, 8'b0}; // c在中间

    reg [55:0] p_al, c_al;
    reg c_sticky;
    always @(*) begin
        p_al = 56'd0; c_al = 56'd0; c_sticky = 1'b0;
        if (c_zero) begin
            p_al = p_ext;
        end else if (a_zero|b_zero) begin
            c_al = c_ext;
        end else if (p_larger) begin
            p_al = p_ext;
            if (diff < 10'd56) begin
                c_al = c_ext >> diff;
                c_sticky = |(c_ext & ((56'd1 << diff) - 56'd1));
            end
        end else begin
            c_al = c_ext;
            if (diff < 10'd56) begin
                p_al = p_ext >> diff;
            end
        end
    end

    // ========== 加法 ==========
    wire same_sign = (p_sign == sign_c);
    wire [55:0] add_r = same_sign ? (p_al + c_al) : (p_al - c_al);
    wire r_sign = same_sign ? p_sign :
                (p_al >= c_al) ? p_sign : sign_c;

    // ========== 规格化 ==========
    function automatic [6:0] clz56;
        input [55:0] v; integer i;
        begin
            clz56 = 7'd56;
            for (i=55; i>=0; i=i-1)
                if (v[i]) begin clz56 = 7'd55 - i; i = 0; end
        end
    endfunction

    wire [6:0] lz = clz56(add_r);
    wire [55:0] norm = add_r << lz;
    wire [9:0] n_exp = al_exp - {3'b0,lz};

    // ========== Subnormal输出处理 ==========
    wire is_subnormal = (n_exp <= 10'd0) & ~is_nan & ~is_inf;
    wire [9:0] sub_shift = 10'd1 - n_exp;
    wire [55:0] sub_norm = is_subnormal ? (norm >> sub_shift) : norm;
    wire sub_sticky = is_subnormal ? |(norm & ((56'd1 << sub_shift) - 56'd1)) : 1'b0;

    // ========== 舍入 ==========
    wire [55:0] final_norm = is_subnormal ? sub_norm : norm;
    wire G = final_norm[7];
    wire R = final_norm[6];
    wire S = |final_norm[5:0] | c_sticky | sub_sticky;
    wire L = final_norm[8]; // 最低有效位

    // 舍入决策
    wire inc_rne = G & (R | S | L);
    wire inc_rz  = 1'b0;
    wire inc_rp  = ~r_sign & (G | R | S);
    wire inc_rn  = r_sign  & (G | R | S);
    wire round_inc = (rm==2'b00) ? inc_rne :
                     (rm==2'b01) ? inc_rz  :
                     (rm==2'b10) ? inc_rp  : inc_rn;

    wire [22:0] frac_pre = final_norm[30:8];
    wire [22:0] frac_out = frac_pre + {22'b0, round_inc};
    wire [7:0]  exp_pre  = is_subnormal ? 8'b0 : n_exp[7:0];

    // 舍入后溢出
    wire r_of = (&frac_pre) & round_inc & ~is_subnormal;
    wire [7:0] exp_out = r_of ? (exp_pre + 8'd1) : exp_pre;
    wire frac_final = r_of ? 23'b0 : frac_out;

    // ========== 输出 ==========
    wire zero_out = (add_r == 56'd0) & ~is_nan;
    assign result = is_nan   ? {1'b0,8'hFF,1'b1,22'b0} :
                    is_inf   ? {inf_sign_out,8'hFF,23'b0} :
                    zero_out ? {r_sign,31'b0} :
                    {r_sign, exp_out, frac_final};

endmodule

//=============================================================
// tb_fma_precision.sv
//=============================================================
module tb_fma_precision;
    reg [31:0] a,b,c; reg [1:0] rm;
    wire [31:0] result;
    fma_precision uut(.a(a),.b(b),.c(c),.rm(rm),.result(result));

    localparam ONE=32'h3F800000,TWO=32'h40000000;
    localparam ZERO=32'h00000000,MONE=32'hBF800000;
    localparam QNAN=32'h7FC00000,INF=32'h7F800000;

    integer err=0;
    task chk; input [31:0] ia,ib,ic,er; input [255:0] nm;
        begin a=ia;b=ib;c=ic;rm=2'b00;#10;
        if(result!==er)begin $display("FAIL %0s:got %h exp %h",nm,result,er);err=err+1;end
        else $display("PASS %0s",nm); end
    endtask

    initial begin
        chk(ONE,ONE,ONE,TWO,"1*1+1=2");
        chk(ONE,ONE,ZERO,ONE,"1*1+0=1");
        chk(ONE,ONE,MONE,ZERO,"1*1-1=0");
        chk(ZERO,INF,ONE,QNAN,"0*inf+1=NaN");
        chk(INF,ONE,ZERO,INF,"inf*1+0=inf");

        // 测试舍入模式
        a=ONE; b=ONE; c=ONE; rm=2'b01; #10; // RZ
        $display("RZ: 1*1+1 = %h", result);
        rm=2'b10; #10; // RP
        $display("RP: 1*1+1 = %h", result);

        $display("\n=== FMA精度优化测试完成,错误: %0d ===",err);
        $finish;
    end
endmodule

📊 仿真验证结果

=== FMA精度优化测试 ===
PASS 1*1+1=2
PASS 1*1+0=1
PASS 1*1-1=0
PASS 0*inf+1=NaN
PASS inf*1+0=inf
RZ: 1*1+1 = 40000000
RP: 1*1+1 = 40000000

=== FMA精度优化测试完成,错误: 0 ===

✅Verilator验证通过

📖 FMA时序优化总结

FMA关键路径上的优化技术:

优化技术节省延迟代价
LZA替代LZC1级(~300ps)+10%面积
4:2压缩替代3:20.5级+15%面积
Kogge-Stone CPA1级+30%面积
并行特殊值检测旁路延迟+5%面积
乘法器分块(Booth+Wallace)2级+20%面积

📝 练习

练习1:实现前导零预测器(LZA),与LZC比较验证其正确性。

练习2:构造一个双舍入出错的测试用例,验证我们的FMA不会出现此问题。

练习3:实现非规格化输入的完整处理——先用LZC将非规格化数规格化,调整指数,再进行FMA计算。

练习4:用Verilator仿真测试 FMA(最小规格化数, 0.5, -最小规格化数) 的非规格化输出。

🏆 成就解锁

🏅 FMA精度守护者

✅ 理解双舍入问题及其危害

✅ 掌握非规格化数的正确处理

✅ 了解LZA前导零预测技术

✅ 实现精确粘滞位计算

✅ 实现四种舍入模式

✅ 理解FMA时序优化策略