Altifigence Academy

53 / 63 · 概念

定点数、舍入与饱和

用 W、F 定义数值范围,设计基于 guard、sticky 的偶数舍入及舍入后饱和。

先修知识:应理解 signed 加法和乘法的位宽、算术右移与溢出。

为位串约定小数点位置

定点数定点数 预先固定小数点位置,并对整数位串施加固定比例的数值表示方法。必须同时规定位宽、符号属性和小数位数。将整数 X 解释为 x=X2−Fx=X2^{-F}。硬件并不存储一个“小数点比特”。本课规定 signed 总位宽 W 包含符号位,小数位数为 F。不同文献的 Q 记法对整数部分是否包含符号位有不同约定,因此在接口中同时写明 W、F 更清楚。

−2W−1−F≤x≤2W−1−F−2−F-2^{W-1-F}\le x\le 2^{W-1-F}-2^{-F}

W=8、F=4 时,间隔为 1/16,范围为 -8~7.9375。在此约定下,0x18 不是整数 24,而是 1.5。同样的 0x18 若按 F=3 解释,则为 3。线路连接正确并不意味着数值含义正确,因此必须记录每个端口的比例因子。

相加前必须先对齐小数点。若 F 不同,可将较小 F 的一方充分扩宽后左移,或按规定舍入方式缩减较大 F 的一方。同一 W、F 的两个数要精确相加,需要 W+1 位并保持 F 不变。乘法则先将原始整数相乘,再把小数位数相加。

P=XAXB,FP=FA+FBP=X_A X_B,\qquad F_P=F_A+F_B

两个八位 F=4 数的完整乘积为 signed 十六位 F=8。若要再输出八位 F=4,应先减少四个小数位,再检查 signed 八位范围。若先把乘积截为八位,后续饱和电路就无法知道原始大小。

截断与舍入的区别

丢弃 d 位的算术右移为 q=⌊P/2d⌋q=\lfloor P/2^d\rfloor,负数同样向下取整。例如 P=−24,d=4P=-24,d=4,精确商为 -1.5,但 q=-2,与向零截断不同。不能假设对正数适用的“直接取高位”,对负数也有相同误差特性。

这里采用就近舍入,恰好位于中点时取偶数整数,即 round-to-nearest, ties-to-even。将舍弃区域的最高位记为 guard g,其余位的 OR 记为 sticky t,保留商 q 的最低位记为 l。

inc=g∧(t∨l),qrnd=q+inc\mathrm{inc}=g\land(t\lor l),\qquad q_{\mathrm{rnd}}=q+\mathrm{inc}

g=0 表示不足一半,保持原值;g=1、t=1 表示超过一半,应进位;g=1、t=0 表示恰好一半,只在 q 为奇数时加 1。负数也用同一规则,因为使用向下取整的商 q 及余数 r=P−q2dr=P-q2^d 时,总有 0≤r<2d0\le r<2^d。

例如 -24 除以 16 得到 -1.5,q=-2 为偶数,所以保留 -2;-40 除以 16 得到 -2.5,q=-3 为奇数,所以加 1 得到 -2。“负数一律减 1”这样的额外规则反而错误。未饱和时,最终数值误差不超过输出 LSBLSB 权重最低的比特。应同时检查表示方向与索引规则。了解更多 的一半。ties-to-even 不保证任意输入分布的平均误差都为 0,但可减小将所有中点都朝同一方向舍入的偏差。

将十六位 F=8 的原始值 P=2040 转为八位 F=4。127.5 先舍入为偶数 128,再限制到 signed 八位最大值 127。直到最后存储之前,都必须保留较宽位宽。

先舍入,再饱和
  1. 输入 P=2040: 十六位 · F=8
  2. 缩减小数位数: 2040 / 16 = 127.5
  3. ties-to-even: 舍入后原始值 = 128
  4. 饱和后存储: raw 127 → 7.9375

完成舍入后再做饱和

饱和将大于最大值的结果限制为最大值、小于最小值的结果限制为最小值。与回绕不同,大正数不会翻成负数。但对已经缩窄并回绕的值,无法再正确实施饱和。

以下完整组合模块将 signed 十六位 F=8 转为 signed 八位 F=4。d 固定为 4,因此 guard 为 p[3],sticky 为 p[2:0]。舍入增量与范围比较都在十七位 signed 域中完成,最后才取八位。

SystemVerilog
module requantize (
  input  logic signed [15:0] p,
  output logic signed [7:0] y,
  output logic saturated
);
  logic signed [16:0] wide, q, rounded;
  logic inc;
  always_comb begin
    wide = {p[15], p};
    q = wide >>> 4;
    inc = p[3] & ((|p[2:0]) | q[0]);
    rounded = q + (inc ? 17'sd1 : 17'sd0);
    saturated = 1'b0;
    if (rounded > 17'sd127) begin
      y = 8'sh7f;
      saturated = 1'b1;
    end else if (rounded < -17'sd128) begin
      y = 8'sh80;
      saturated = 1'b1;
    end else begin
      y = rounded[7:0];
    end
  end
endmodule

P=2040 时,P/16=127.5,先舍入到偶数 128,再饱和到 127,最终值为 7.9375。若只检查舍入前的 q=127,就会漏掉此次越界。saturated 表示数值超出范围而被限制,并不只是丢失了低位小数。

推广为通用模块时,需分别处理 d=0 没有舍弃位、d=1 没有 sticky 区域的情况。仅把越界的部分选择藏在条件语句内,可能仍无法解决展开阶段的错误。

舍入与饱和会增加加法器、比较器、MUX多路选择器 根据选择信号,把多个输入之一连接到输出的组合电路。必须规定选择位与输入编号的对应关系。了解更多的成本与延迟,且应作为独立策略选择。AMD 定点格式文档也区分这些策略。累加多项时,应先决定是否保留中间位宽、最后只转换一次。每一级都饱和可能破坏加法结合律,每一级都舍入则可能重复累积小误差。

自己试试

将 signed 十六位 F=8 输入 P 转为 signed 八位 F=4。P 分别为 24、40、−24、−40、2040 时,求向下取整的商 q、guard/sticky、舍入后整数、最终 raw 输出及实际值。对 2040 若先检查饱和,会出现什么问题?

阅读解释

所有输入的低四位均为 1000,因此 guard=1、sticky=0。P=24:q=1(奇数)→2,raw=2,实际值为 0.125。P=40:q=2(偶数)→2,raw=2,实际值为 0.125。P=−24:q=−2(偶数)→−2,raw=−2,实际值为 −0.125。P=−40:q=−3(奇数)→−2,raw=−2,实际值为 −0.125。P=2040:q=127(奇数)→128,超出 signed 八位最大值,所以 raw=127、实际值=7.9375、saturated=1。若先只检查 q=127,再在八位范围内加 1,就可能回绕成 0x80 并输出 −8。必须在较宽的 signed 值中完成增量与比较。

你的选择适用于此浏览器,可随时在页脚更改。