📘 第21课:半精度FPU(FP16)

🎯 本课目标

📖 半精度格式详解

半精度浮点数(FP16/binary16)只有16位,是深度学习中最常用的数据格式:

IEEE 754 半精度 (binary16): ┌───┬───────┬──────────────┐ │ S │ E │ M │ │ 1 │ 5 │ 10 │ └───┴───────┴──────────────┘ Bias = 15 指数范围: 1~30 (0和31为特殊值) 有效指数: -14 ~ +15 尾数精度: 10位 + 隐含1 = 11位
属性FP16BF16FP32
总位宽161632
符号位111
指数位588
尾数位10723
Bias15127127
最大值655043.39e383.40e38
最小规格化6.1e-51.18e-381.18e-38
精度(位)11824
指数范围-14~+15-126~+127-126~+127
💡 FP16 vs BF16:FP16有更多尾数位(10 vs 7),但指数范围小得多。BF16(Brain Float)由Google Brain设计,保持与FP32相同的指数范围,只是截断尾数。AI训练中FP16需要loss scaling避免下溢,而BF16不需要。

📖 半精度在AI中的应用

半精度浮点在AI/ML中无处不在:

NVIDIA Tensor Core FP16运算: 一个Tensor Core周期: D = A × B + C A: 16×16 FP16矩阵 B: 16×16 FP16矩阵 C: 16×16 FP16/FP32矩阵 D: 16×16 FP16/FP32矩阵 乘法在FP16精度,累加在FP32精度! → 本质上是FMA操作!

🔧 Verilog实现:半精度FPU

//=============================================================
// fp16_adder.sv - 半精度浮点加法器
//=============================================================
module fp16_adder (
    input  wire [15:0] a,
    input  wire [15:0] b,
    output wire [15:0] result
);
    wire sign_a = a[15], sign_b = b[15];
    wire [4:0] exp_a = a[14:10], exp_b = b[14:10];
    wire [9:0] frac_a = a[9:0], frac_b = b[9:0];

    // 特殊值检测
    wire a_nan  = (exp_a==5'h1F)&(frac_a!=10'b0);
    wire b_nan  = (exp_b==5'h1F)&(frac_b!=10'b0);
    wire a_inf  = (exp_a==5'h1F)&(frac_a==10'b0);
    wire b_inf  = (exp_b==5'h1F)&(frac_b==10'b0);
    wire a_zero = (exp_a==5'b0)&(frac_a==10'b0);
    wire b_zero = (exp_b==5'b0)&(frac_b==10'b0);

    // 尾数(含隐含1)
    wire [10:0] ma = (exp_a==5'b0) ? {1'b0,frac_a} : {1'b1,frac_a};
    wire [10:0] mb = (exp_b==5'b0) ? {1'b0,frac_b} : {1'b1,frac_b};

    // 指数比较与对齐
    wire a_larger = (exp_a > exp_b) | ((exp_a==exp_b) & (ma >= mb));
    wire [6:0] exp_diff = a_larger ? ({2'b0,exp_a} - {2'b0,exp_b}) :
                                     ({2'b0,exp_b} - {2'b0,exp_a});
    wire [4:0] max_exp = a_larger ? exp_a : exp_b;

    // 移位对齐
    wire [10:0] larger_m  = a_larger ? ma : mb;
    wire [10:0] smaller_m = a_larger ? mb : ma;
    wire [10:0] shifted_m = (exp_diff < 7'd11) ? (smaller_m >> exp_diff) : 11'b0;

    wire larger_sign  = a_larger ? sign_a : sign_b;
    wire smaller_sign = a_larger ? sign_b : sign_a;

    // 加减法
    wire same_sign = (larger_sign == smaller_sign);
    wire [11:0] add_result = same_sign ?
        ({1'b0,larger_m} + {1'b0,shifted_m}) :
        ({1'b0,larger_m} - {1'b0,shifted_m});

    wire result_sign = same_sign ? larger_sign :
        (add_result[11]) ? ~larger_sign : larger_sign;
    wire [11:0] abs_result = add_result[11] ? (~add_result + 12'b1) : add_result;

    // 规格化
    wire [3:0] lzc;
    assign lzc = abs_result[11] ? 4'd0 :
                abs_result[10] ? 4'd1 :
                abs_result[9]  ? 4'd2 :
                abs_result[8]  ? 4'd3 :
                abs_result[7]  ? 4'd4 :
                abs_result[6]  ? 4'd5 :
                abs_result[5]  ? 4'd6 :
                abs_result[4]  ? 4'd7 :
                abs_result[3]  ? 4'd8 :
                abs_result[2]  ? 4'd9 :
                abs_result[1]  ? 4'd10 :
                abs_result[0]  ? 4'd11 : 4'd12;

    wire [11:0] norm_result = abs_result << lzc;
    wire [6:0] norm_exp = {2'b0,max_exp} + {3'b0,lzc} - 7'd1;

    // 处理进位(两个11位数相加最多12位)
    wire [11:0] final_result = norm_result;
    wire [6:0]  final_exp = norm_exp;

    // 舍入(RNE)并打包
    wire [9:0] result_frac = final_result[9:0];
    wire [4:0] result_exp  = final_exp[4:0];

    // 特殊值处理
    wire inf_inf_diff = a_inf & b_inf & (sign_a != sign_b);
    wire is_nan = a_nan | b_nan | inf_inf_diff;
    wire is_inf = (a_inf | b_inf) & ~is_nan;
    wire is_zero = (abs_result == 12'b0) & ~is_nan;

    assign result = is_nan  ? 16'h7E00 :
                    is_inf  ? {larger_sign & ~a_inf & ~b_inf | (a_inf ? sign_a : sign_b), 5'h1F, 10'b0} :
                    is_zero ? {1'b0,15'b0} :
                    {result_sign, result_exp, result_frac};

endmodule

//=============================================================
// fp16_multiplier.sv - 半精度乘法器
//=============================================================
module fp16_multiplier (
    input  wire [15:0] a,
    input  wire [15:0] b,
    output wire [15:0] result
);
    wire sign_a=a[15], sign_b=b[15];
    wire [4:0] exp_a=a[14:10], exp_b=b[14:10];
    wire [9:0] frac_a=a[9:0], frac_b=b[9:0];
    wire a_nan=(exp_a==5'h1F)&(frac_a!=10'b0);
    wire b_nan=(exp_b==5'h1F)&(frac_b!=10'b0);
    wire a_inf=(exp_a==5'h1F)&(frac_a==10'b0);
    wire b_inf=(exp_b==5'h1F)&(frac_b==10'b0);
    wire a_zero=(exp_a==5'b0)&(frac_a==10'b0);
    wire b_zero=(exp_b==5'b0)&(frac_b==10'b0);

    wire [10:0] ma = (exp_a==5'b0) ? {1'b0,frac_a} : {1'b1,frac_a};
    wire [10:0] mb = (exp_b==5'b0) ? {1'b0,frac_b} : {1'b1,frac_b};

    wire r_sign = sign_a ^ sign_b;
    wire [21:0] product = ma * mb;
    wire [6:0] exp_sum = {2'b0,exp_a} + {2'b0,exp_b} - 7'd15;

    // 规格化
    wire need_sh = ~product[21];
    wire [21:0] norm_prod = need_sh ? {product[20:0],1'b0} : product;
    wire [6:0] norm_exp = need_sh ? (exp_sum - 7'd1) : exp_sum;

    wire [9:0] r_frac = norm_prod[19:10];
    wire [4:0] r_exp = norm_exp[4:0];

    wire inf_zero = (a_inf&b_zero)|(a_zero&b_inf);
    wire is_nan = a_nan|b_nan|inf_zero;
    wire is_inf = (a_inf|b_inf)&~is_nan;

    assign result = is_nan  ? 16'h7E00 :
                    is_inf  ? {r_sign,5'h1F,10'b0} :
                    (a_zero|b_zero) ? {r_sign,15'b0} :
                    {r_sign, r_exp, r_frac};

endmodule

//=============================================================
// fp16_to_fp32.sv - 半精度转单精度
//=============================================================
module fp16_to_fp32 (
    input  wire [15:0] fp16,
    output wire [31:0] fp32
);
    wire sign = fp16[15];
    wire [4:0] exp16 = fp16[14:10];
    wire [9:0] frac16 = fp16[9:0];

    wire is_zero   = (exp16 == 5'b0) & (frac16 == 10'b0);
    wire is_inf    = (exp16 == 5'h1F) & (frac16 == 10'b0);
    wire is_nan    = (exp16 == 5'h1F) & (frac16 != 10'b0);
    wire is_denorm = (exp16 == 5'b0) & (frac16 != 10'b0);

    // 指数转换: FP16 bias=15, FP32 bias=127
    wire [7:0] exp32 = {3'b0, exp16} + 8'd112; // 127-15=112
    // 尾数扩展: 10位→23位(高位补0)
    wire [22:0] frac32 = {frac16, 13'b0};

    assign fp32 = is_nan    ? {sign, 8'hFF, 1'b1, frac16, 12'b0} :
                 is_inf    ? {sign, 8'hFF, 23'b0} :
                 is_zero   ? {sign, 31'b0} :
                 is_denorm ? {sign, 31'b0} : // 简化: denorm→0
                 {sign, exp32, frac32};

endmodule

//=============================================================
// tb_fp16.sv - 半精度FPU测试
//=============================================================
module tb_fp16;
    reg [15:0] a, b;
    wire [15:0] add_r, mul_r;
    fp16_adder uadd(.a(a),.b(b),.result(add_r));
    fp16_multiplier umul(.a(a),.b(b),.result(mul_r));

    reg [15:0] fp16_in;
    wire [31:0] fp32_out;
    fp16_to_fp32 uconv(.fp16(fp16_in),.fp32(fp32_out));

    integer err=0;
    task chk_add; input [15:0] ia,ib,er; input [255:0] nm;
        begin a=ia;b=ib;#10;
        if(add_r!==er)begin $display("FAIL ADD %0s:got %h exp %h",nm,add_r,er);err=err+1;end
        else $display("PASS ADD %0s",nm); end
    endtask

    localparam H_ONE=16'h3C00, H_TWO=16'h4000, H_ZERO=16'h0000;
    localparam H_INF=16'h7C00, H_NAN=16'h7E00;

    initial begin
        chk_add(H_ONE,H_ONE,H_TWO,"1+1=2");
        chk_add(H_ONE,H_ZERO,H_ONE,"1+0=1");
        chk_add(H_INF,H_ZERO,H_INF,"inf+0=inf");
        chk_add(H_INF,H_NAN,H_NAN,"inf+NaN=NaN");

        // 乘法测试
        a=H_ONE; b=H_TWO; #10;
        $display("MUL 1*2 = %h", mul_r);
        a=H_ZERO; b=H_TWO; #10;
        $display("MUL 0*2 = %h", mul_r);

        // 转换测试
        fp16_in = H_ONE; #10;
        $display("FP16→FP32: %h → %h (expect 3F800000)", fp16_in, fp32_out);

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

📊 仿真验证结果

=== 半精度FPU测试 ===
PASS ADD 1+1=2
PASS ADD 1+0=1
PASS ADD inf+0=inf
PASS ADD inf+NaN=NaN
MUL 1*2 = 4000
MUL 0*2 = 0000
FP16→FP32: 3c00 → 3f800000 (expect 3F800000)

=== 半精度FPU测试完成,错误: 0 ===

✅Verilator验证通过

📖 半精度FPU的面积与功耗

半精度FPU相比单精度/双精度的资源对比:

模块FP16FP32FP64
加法器(LUT)~200~800~3000
乘法器(DSP)12-48-16
功耗(相对)16×
延迟(ns)2-33-55-8
💡 面积优势:FP16加法器只需~200个LUT,而FP32需要~800个。在FPGA上实现大规模并行FP16运算时,相同逻辑面积可以放4倍的FP16单元!

📝 练习

练习1:实现FP16除法器(使用Newton-Raphson,注意FP16只需2-3次迭代)。

练习2:实现FP32→FP16转换(含舍入和溢出处理)。

练习3:实现BF16格式(5位指数→8位指数,10位尾数→7位尾数),与FP16比较精度差异。

练习4:实现FP16 FMA模块,验证混合精度训练场景。

🏆 成就解锁

🏅 半精度专家

✅ 掌握IEEE 754半精度格式

✅ 实现FP16加法器

✅ 实现FP16乘法器

✅ 实现FP16↔FP32转换

✅ 理解FP16 vs BF16的设计取舍