跳转至

VFPU

  • 版本:V2R2
  • 状态:OK
  • 日期:2025/01/20
  • commit:xxx

术语说明

缩写 全称 描述
VFPU Vector Float Point Unit 向量浮点功能单元
IQ Issue Queue 发射队列

设计规格

  1. 支持向量浮点 Mul 计算
  2. 支持向量浮点 FMA 计算
  3. 支持向量浮点 Div 计算
  4. 支持向量浮点 Sqrt 计算
  5. 支持 fp32,fp64,fp16 计算
  6. 支持 RV-V1.0 版本向量浮点指令的计算

功能

VFPU 模块接收来自 Issue Queue 发射的 uop 信息,根据 fuType 和 fuOpType 等信息,完成向量浮点指令的计算,主要包括:VFAlu、VFMA、VFDivSqrt、VFCvt 四个模块。

VFAlu 主要负责fadd相关的指令和一些其他简单指令,如比较指令、符号注入指令,特别的是reduction sum 指令也通过拆uop的方式在此模块进行计算。

VFMA 主要负责乘法和乘加相关的指令。

VFDivSqrt 主要负责除法和开方相关的指令。

VFCvt 主要负责格式转换和倒数估计相关的指令。

算法设计

向量浮点运算单元的难点在于支持多组单一精度格式(操作数和结果的浮点数格式相同)的计算以及支持混合精度(操作数和结果的浮点数格式不同)的计算,以常见的半精度(\(f16\))、单精度(\(f32\))、双精度(\(f64\))为例,对比标量浮点运算单元和向量浮点运算单元的区别。

以典型的浮点加法为例,对于标量浮点运算单元,只需支持三种单一精度格式的计算,该运算单元输入的操作数和输出的结果都应该是 \(64\) 位的,那么它要支持三种格式的计算:

(1)\(1\)\(f64 = f64 + f64\)

(2)\(1\)\(f32 = f32 + f32\)

(3)\(1\)\(f16 = f16 + f16\)

看似需要三个模块完成这三种格式的计算,但是因为浮点数都是有符号位、阶码、尾数三部分组成,并且高精度浮点数的阶码位宽和尾数位宽都要比低精度的更大,这就使得高精度浮点数的硬件设计可以完全满足低精度浮点数的硬件需求,稍加处理之后就可以通过在硬件中添加 \(Mux\)(多路选择器)的方式兼容多种单一精度格式的计算,并且面积只会略微增加。

而向量浮点运算单元要支持向量操作,向量操作的一个特点是对数据带宽利用率高,比如标量运算单元虽然接口是 \(64\) 位的,但在计算 \(f32/f16\) 时,其有效数据只有 \(32/16\) 位,带宽利用率骤减到 \(50\%/25\%\);向量运算单元接口也是 \(64\) 位的,但是在计算单一精度格式 \(f32/f16\) 时,它可以同时进行 \(2/4\) 组操作,带宽利用率还是可以达到 \(100\%\),支持的单一精度格式计算如下:

(1)\(1\)\(f64 = f64 + f64\)

(2)\(2\)\(f32 = f32 + f32\)

(3)\(4\)\(f16 = f16 + f16\)

需要同时进行多组相同格式的浮点数相加使得硬件设计比标量更困难,但是也能将高精度格式的硬件复用来计算低精度格式。另外,向量浮点运算单元还要支持的一个特性是混合精度计算,\(RISC-V\) 向量指令集拓展定义了一系列需要混合精度计算的 \(widening\) 指令,要求浮点加法运算单元还要支持以下四种计算格式:

(1)\(1\)\(f64 = f64 + f32\)

(2)\(1\)\(f64 = f32 + f32\)

(3)\(2\)\(f32 = f32 + f16\)

(4)\(2\)\(f32 = f16 + f16\)

混合精度计算的设计难度比多组单一精度格式要大得多。一方面,不同的数据格式操作数需要进行格式转换,转换成和结果相同的格式再计算,逻辑复杂度提高了;另一方面,格式转换对电路时序的压力非常大,尤其是将低精度的非规格化数转换成高精度浮点数时,因此本文专门设计了一种快速数据格式转换算法来解决时序问题。

综上所述,向量浮点运算器的设计难点在于多组单一精度格式的实现和混合精度格式的实现。本节将逐一介绍向量浮点加法算法、浮点顺序累加算法、向量浮点融合乘加算法、向量浮点除法算法来解决向量浮点运算器的设计难点,实现频率可达 \(3GHz\) 的高性能向量浮点运算器。

向量浮点加法

浮点加法是科学计算中最常用的算术运算之一。尽管概念简单,但是传统的单通路浮点加法算法需要两到三个有符号加法步骤,这是一个相对耗时的操作。双通路浮点加法算法在最坏情况下关键路径上只有一个有符号加法操作,因此与单通路算法相比具有很大的速度优势,本文在双通路浮点加法算法的基础上,设计了一种更为快速的改进的双通路浮点加法算法。本节先介绍单一精度格式的单通路浮点加法算法、双通路浮点加法算法、改进的双通路浮点加法算法,最后介绍向量浮点加法算法。

浮点加法公式表示为:\(fp\_result = fp\_a + fp\_b\)。当 \(fp\_a\)\(fp\_b\) 符号相同时,有效数字对齐后做加法,这种情况称作等效加法;当 \(fp\_a\)\(fp\_b\) 符号不同时,有效数字对齐后做减法,这种情况称作等效减法。对于非规格化数阶码等于 \(0\),和规格化数阶码等于 \(1\) 对应的规格化的指数是一样的,所以计算指数差时,阶码为 \(0\) 时应当作1处理(称为规格化的阶码),规格化的阶码之差的绝对值为规格化的指数之差。

单通路浮点加法算法

传统的单通路浮点加法运算如图所示,由以下步骤组成:

单通路浮点加法算法

(1)规格化的指数减法(Exponent subtraction,\(ES\)):求规格化的指数之差 \(d = |Ea - Eb|\)\(Ea\)\(Eb\) 都是规格化的阶码。

(2)对齐(Alignment,\(Align\)):将较小的操作数的有效数字右移 \(d\) 位。较大的指数记作 \(Ef\)

(3)有效数字加法(Signicand addition,\(SA\)):根据有效操作 \(Eo\) 执行加法或减法,\(Eo\) 是浮点加法单元中加法器实际执行的算术运算,根据两浮点操作数的符号位确定。

(4)转换(Conversion,\(Conv\)):如果有效数字加法结果为负,则将结果转换为符号-幅度表示。转换是通过一个加法步骤完成的,转换结果表示为 \(Sf\)

(5)前导零检测(Leading zero detection,\(LZD\)):计算所需的左移或右移量,并将其表示为 \(En\),右移为正,否则为负。

(6)归一化(Normalization,\(Norm\)):通过移 \(En\) 位将有效数字归一化,并将 \(En\) 加到\(Ef\) 上。

(7)舍入(Rounding,\(Round\)):根据 \(IEEE\)-\(754\) 标准舍入,必要时在 \(Sf\)\(LSB\) 上加 \(1\),此步骤可能导致溢出,需要将尾数结果右移一位,同时指数 \(Ef\)\(1\)

双通路浮点加法算法

上述单通路浮点算法很慢,因为加法运算中的步骤基本上都是串行执行的,可以通过以下方式改进该算法:

(1)在单通路浮点加法算法中,仅当结果为负时才需要 \(Conv\) 步骤,并且可以通过交换两操作数的有效数字来避免 \(Conv\) 步骤。通过检查 \(ES\) 步骤结果的正负号,并可以相应的交换(\(Swap\))有效数字,总是计算较大的有效数字减去较小的有效数字。在指数相等的情况下,结果仍可能为负,需要进行转换,但在这种情况下不需要舍入。因此,交换步骤使舍入和转换相互排斥,允许将它们并行起来。注意交换还有一个优点是只需要一个移位器。

(2)前导零检测步骤可以与有效数字加法步骤并行执行,将其从关键路径中移除。在计算减法的情况下,当需要大量左移时,这种优化非常重要。

(3)到目前为止,已经能够将关键路径步骤减少为:规格化的指数减法,交换,对齐,有效数字加法 \(||\) 前导零检测, 转换 \(||\) 舍入,归一化( \(||\) 表示可以并行执行的步骤)。对齐和归一化步骤是互斥的,可以进一步优化,只有当 \(d≤1\) 时或者等效减法时,归一化需要大量的左移。相反,只有当 \(d > 1\) 时,对齐步骤需要大量的右移。通过区分这两种情况,只有一个大量的移位关键路径,对齐或归一化。

单通路浮点加法算法和双通路浮点加法算法中的步骤如表所示。在双通路浮点加法算法中,\(d≤1\) 路径中的预处理步骤(\(Pred\))根据 \(d\) 的值决定是否需要右移一位来对齐有效数字。双通路浮点加法算法通过并行执行更多的步骤来提高速度,因此需要更多的硬件来实现。

两种浮点加法算法步骤

单通路浮点加法


:================:
规格化的指数加法

双通路浮点加法算法

\(d\leq1\) 且 等效减法
:===========================:
预处理+交换

\(d>1\) 或 等效加法
:=====================:
规格化的指数减法+交换

对齐

--

对齐

有效数字加法

有效数字加法 或 前导零检测

有效数字加法

转换

转换 或 舍入

舍入

前导零检测

--

--

归一化

归一化

--

舍入

选择路径

选择路径

在双通路浮点加法算法中,\(SA\) 步骤在等效减法的情况下,其中一个有效数字是 \(2\) 的补码,该求补码步骤和舍入步骤是互斥的,因此可以并行,优化后的双通路浮点加法算法见表。

优化后的双通路浮点加法算法
\(d≤1\) 且 等效减法 \(d>1\) 或 等效加法
预处理+交换 规格化的指令减法+交换
有效数字加法转换\(\|\) 舍入 \(\|\) 前导零检测 对齐
归一化 有效数字加法\(\|\) 舍入
选择路径 选择路径

\(IEEE\) 舍入到最近值(\(RTN\))模式中,计算 \(A+B\)\(A+B+1\) 足以解决所有归一化可能性(向正负无穷方向舍入时还需额外计算 \(A+B+2\))。通过使用 \(Cin\) 从多组有效数字加法器结果中选择最终尾数舍入后的结果,可以同时完成求补码和舍入,从而节省一个加法步骤。由于在浮点加法中,结果的归一化可能需要右移一位、不移位或左移,左移的位数可能与有效数字的长度一样多,因此 \(Cin\) 需要考虑所有这些归一化的可能性,使得最终选择的结果是舍入后的结果。

改进的双通路浮点加法算法

本节详细介绍本文改进的双通路浮点加法算法,等效加法或 \(d>1\) 的等效减法的通路称为 \(far\) 路径,\(d≤1\) 且等效减法的通路称为 \(close\) 路径。含有无穷或 \(NaN\) 操作数的情况单独判断,不属于 \(far\) 路径或 \(close\) 路径。

far 路径

\(far\) 路径算法如图所示,其中主要步骤如下:

far 路径算法示意图

第一步,\(far\) 路径下指数差 \(d\) 大于 \(1\) ,要对较小的有效数字右移d位来对齐较大的有效数字,首先计算规格化的指数差,为了加快计算速度,使用两个加法器来计算规格化的指数差,同时进行 \(Efp\_a\)\(Efp\_b\) 大小比较,根据指数大小的比较结果选择出正确的规格化的指数差。

第二步,根据第一步中指数大小比较关系,可以与第一步求规格化的指数差并行选择出指数较大的操作数的有效数字和指数较小的操作数的有效数字,同时选出较大的指数 \(EA\),当等效减法时,\(EA\) 自减 \(1\)(此时 \(EA\) 不可能等于 \(0\),因为如果等于 \(0\) 就是 \(close\) 路径了),这样做的目的是调整有效数字做完减法后的值域,和等效加法值域统一起来方便后面选择出最终结果。调整后有效数字加或减的结果值域在 \([1\)-\(4)\) 之间,分为两种情况:\([1\)-\(2)\) 之间、\([2\)-\(4)\) 之间。

第三步,对较小的有效数字右移,此处右移分两种情况:在等效减法时,对较小的有效数字先取反再算数右移,比先右移再取反节省了一些时间;等效加法时之间逻辑右移。为了节省右移器的级数,当规格化的指数差的高位全 \(0\) 时,右移时用规格化的指数差的低位(具体位数取决于有效数字宽度)进行右移,当规格化的指数差的高位不是全 \(0\) 时,右移结果为 \(0\)。这里使用第一步中的求两个规格化的指数差的加法器结果,并且先使用最低位进行右移(因为加法结果最低位最先得到),具体如下:假设 \(fp\_a\) 指数大,则只需对 \(fp\_b\) 的有效数字进行右移,右移的值就是 \(fp\_a\) 的规格化的指数减 \(fp\_b\) 的规格化的指数;假设 \(fp\_b\) 指数大,则只需对 \(fp\_a\) 的有效数字进行右移,右移的值就是 \(fp\_b\) 的规格化的指数减 \(fp\_a\) 的规格化的指数。然后根据指数大小关系和规格化的指数差的值选出最终右移后的有效数字,并且计算出右移后的 \(grs\)\(guard\)\(round\)\(sticky\))位。此处为了对第二步的两种情况正确舍入,需要计算有效数字加减后结果在 \([1\)-\(2)\) 之间,\([2\)-\(4)\) 之间的两组 \(grs\)

第四步,进行有效数字加法,因为等效减法时较小的有效数字在右移前已经取反了,此处记较大的有效数字为 \(A\),较小的有效数字右移后为 \(B\),两个有效数字加法器分别计算 \(A+B\)\(A+B+2\),最终舍入结果从这两个加法器结果中选择出来。

第五步,产生最终结果,根据有效数字 \(A+B\) 的结果在 \([1\)-\(2)\) 之间(情况一)还是 \([2\)-\(4)\)之间(情况二),根据之前右移中计算的两套 \(grs\) 和舍入模式,分别确定情况一选择两个有效数字加法器的条件,情况二选择两个有效数字加法器的条件,最后用四选一的独热码选择出尾数结果。指数结果则是 \(EA\)(情况一且尾数舍入后 \(<1\))或 \(EA+1\) (情况二或情况一舍入后 \(=2\)),注意舍入后指数是否 \(overflow\),最终结果由 \(overflow\) 选出 \(overflow\) 结果和正常计算结果。异常标志位 \(far\) 路径下只会产生上溢和不精确。

close 路径

\(close\) 路径下一定是等效减法并且 \(d≤1\) 的情况,具体细分为 \(d=0\)\(d=1\),算法如图所示,具体步骤如下:

close 路径算法示意图

第一步,并行做四组有效数字减法,根据 \(d=0\)\(fp\_a\) 尾数大,\(fp\_b\) 尾数大),\(d=1\)\(fp\_a\) 规格化的指数大,\(fp\_b\) 规格化的指数大),组合出有效减法的四种情况。第一个减法器:\(fp\_a\) 有效数字 \(-\) \(fp\_b\) 有效数字;第二个减法器:\(fp\_b\) 有效数字 \(-\) \(fp\_a\) 有效数字;第三个减法器:\(fp\_a\) 有效数字\(×2\) \(-\) \(fp\_b\) 有效数字;第四个减法器:\(fp\_b\) 有效数字\(×2\) \(-\) \(fp\_a\) 有效数字。同时根据指数大小关系计算出 \(grs\) 位,\(d=0\)\(grs\) 全为 \(0\)\(d=1\) 时只有 \(g\) 可能非 \(0\)。这四组加法器不能产生所有舍入的结果,增加第五个慢速加法器,指数大的有效数字 \(–\) 指数小的有效数字右移一位。

第二步,确定选择四组有效数字减法的四个条件,根据 \(d\) 的值,加法器结果的最高位,\(grs\) 和舍入模式来确定。从四组加法器中选出减法结果后需要对减法结果进行 \(LZD\) \(+\) 左移,这里要注意较大指数 \(EA\) 的值,左移要受 \(LZD\)\(EA\) 共同控制,根据 \(EA\) 的值产生一个 \(mask\) 值(与减法结果位宽相同但最多只有一比特是 \(1\)),与减法结果或操作后再进行 \(LZD+\) 左移即可。

第三步,确定选择第五个减法器的条件,选择第五个减法器结果时是不需要左移的,所以采用的慢速加法器,最终的尾数结果就可以选择出来了。

第四步,指数结果和符号位结果,指数位结果需要用 \(EA\) 减第二步中的 \(LZD\) 的值,如果选择的是第五个减法器作为尾数结果,则指数保持原值。当 \(d=1\) 时符号位的取值就是指数较大的操作数的符号,\(d=0\) 时要根据尾数大小选择符号位,注意结果为 \(0\) 且向下舍入时,符号位为 \(1\)

向量浮点加法算法

向量浮点加法器输出信号宽度为 \(64\),支持混合精度,支持 \(widening\) 指令,共要支持如下数据格式计算:

(1)\(1\)\(f64\) \(= f64 + f64\);

(2)\(1\)\(f64\) \(= f64 + f32\);

(3)\(1\)\(f64\) \(= f32 + f32\);

(4)\(2\)\(f32\) \(= f32 + f32\);

(5)\(2\)\(f32\) \(= f32 + f16\);

(6)\(2\)\(f32\) \(= f16 + f16\);

(7)\(4\)\(f16\) \(= f16 + f16\)

模块划分

计算思路是用一个模块计算前三种格式,它们输出的结果都是 \(64\) 位,将计算 \(f64 = f64 + f64\) 的单精度浮点加法器复用,使之能计算 \(f64 = f64 + f32\)\(f64 = f32 + f32\),本文提出一种快速的数据格式转换算法,将 \(f32\) 操作数转换为 \(f64\) 后就可以进行 \(f64 = f64 + f64\) 计算,得到 \(f64\) 的结果格式。

对于输出结果是 \(f32\) 的计算格式也采取同样的思路进行,因为 \(f32\) 的时序压力没那么大,将一个 \(f16 = f16 + f16\) 也融入到计算结果为 \(f32\) 的模块节约面积,使它支持:

(1)\(1\)\(f32 = f32 + f32\);

(2)\(1\)\(f32 = f32 + f16\);

(3)\(1\)\(f32 = f16 + f16\);

(4)\(1\)\(f16 = f16 + f16\)

显然这个模块需要例化两个,最后还差 \(2\)\(f16 = f16 + f16\),单独例化两个只计算 \(f16 = f16 + f16\) 的单精度浮点加法器,一共四个模块,实现所有向量加法计算格式。

快速格式转换算法

\(f16\) 转换成 \(f32\) 为例,介绍快速格式转换算法。

\(f16\) 为规格化数时,转换成 \(f32\) 也一定是规格化数。对于 \(f16\) 指数要偏置到 \(f32\) 的指数,因为 \(f32\) 的指数范围更大,所以不用担心指数转换之后出现越界的问题,另外 \(f16\) 尾数是 \(10\) 位的, \(f32\) 尾数是 \(23\) 位的,只需在 \(f16\) 尾数后补 \(13\) 个零就可以得到 \(f32\) 的尾数,并且这是一个低精度向高精度的转换,结果肯定是精确的。

对于规格化的 \(f16\) 的指数(位宽是 \(5\) ),实际指数 \(Ereal = Ef16 – 15\),对于规格化的 \(f32\) 的指数(位宽是 \(8\)),\(Ereal = Ef32 – 127\),所以借助 \(Ereal\)\(Ef16\) 转换到 \(Ef32\)\(Ef16 – 15 = Ef32 – 127\)\(Ef32 = Ef16 – 15 + 127\)\(Ef32 = Ef16 + 112\)\(112\)\(8\) 位二进制表示为 \(01110000\),计算 \(Ef16 + 112\) 需要一个变量加一个常数的加法器,通过发现规律可以避免这个加法器,规律如下:

\(Ef16\) 最高位是 \(0\) 时,\(Ef16 + 112 =(0111,Ef16(3,0))\)

\(Ef16\) 最高位是 \(1\) 时,\(Ef16 + 112 =(1000,Ef16(3,0))\)

通过这个规律可以用一个 \(Mux\) 快速进行 \(Ef16\)\(Ef32\) 的转换,所以对于规格化数 \(f16\) 转换为 \(f32\) 是很快的,指数位用一个 \(Mux\),尾数位补 \(0\),符号位不变。难点在于当 \(f16\) 是非规格化数的时候,此时 \(f16\) 指数全为 \(0\),而尾数的前导零个数决定了转换成 \(f32\) 的指数,当 \(f16\) 的指数位全是零,尾数位只有 \(lsb\)\(1\) 时,转换成 \(f32\) 的指数最小,为 \(-15-9=-24\),此时仍在 \(f32\) 规格化数内。所以对于 \(f16\) 非规格化数,要进行尾数的前导零检测 \(lzd\) 和左移。

\(Chisel\) 自带的优先级编码可以实现 \(lzd\) 功能,本文实测比传统使用二分法写的 \(lzd\) 综合效果更好。语法为:\(PriorityEncoder(Reverse(Cat(in,1.U)))\)\(in\) 的位宽为 \(5\) 生成的 \(verilog\) 代码如下:

module LZDPriorityEncoder(
  input        clock,
  input        reset,
  input  [4:0] in,
  output [2:0] out
);
  wire [5:0] _out_T = {in,1'h1};
  wire [5:0] _out_T_15 = {_out_T[0],_out_T[1],_out_T[2],_out_T[3],_out_T[4],_out_T[5]};
  wire [2:0] _out_T_22 = _out_T_15[4] ? 3'h4 : 3'h5;
  wire [2:0] _out_T_23 = _out_T_15[3] ? 3'h3 : _out_T_22;
  wire [2:0] _out_T_24 = _out_T_15[2] ? 3'h2 : _out_T_23;
  wire [2:0] _out_T_25 = _out_T_15[1] ? 3'h1 : _out_T_24;
  assign out = _out_T_15[0] ? 3'h0 : _out_T_25;
endmodule

虽然这段代码看起来使用了很多级联的 \(Mux\),但是综合器对于这种代码综合的时序比较好。受此启发,本文设计了一种新型基于优先级的左移算法来加快 \(lzd+\) 左移,其 \(Chisel\) 代码如下:

def shiftLeftPriorityWithF32EXPResult(srcValue: UInt, priorityShiftValue: UInt): UInt = {
  val width = srcValue.getWidth
  val lzdWidth = srcValue.getWidth.U.getWidth
  def do_shiftLeftPriority(srcValue: UInt, priorityShiftValue: UInt, i:Int): UInt = {
    if (i==0) Cat(
      Mux(
        priorityShiftValue(i),
        Cat(srcValue(0),0.U((width-1).W)),
        0.U(width.W)
      ),
      Mux(
        priorityShiftValue(i),
        "b01110000".U-(width-i-1).U(8.W),
        "b01110000".U-(width-i).U(8.W)
      )
    )
    else Mux(
      priorityShiftValue(i),
      if (i==width-1) Cat(srcValue(i,0),"b01110000".U-(width-i-1).U(8.W)) 
      else Cat(Cat(srcValue(i,0),0.U((width-1-i).W)), "b01110000".U-(width-i-1).U(8.W)),
      do_shiftLeftPriority(srcValue = srcValue, priorityShiftValue = priorityShiftValue, i = i - 1)
      )
    }
    do_shiftLeftPriority(srcValue = srcValue, priorityShiftValue = priorityShiftValue, i = width-1)
  }

\(srcValue\)\(priorityShiftValue\) 都传 \(f16\) 的尾数,从尾数最高位开始进行判断,尾数最高位为 \(1\) 则返回 \(srcValue\) 原值,同时返回相应的指数(指数是从多个常数中选一个,和尾数中第一个 \(1\) 出现的位置有关),如果最高位是 \(0\) ,则接着判断下一位是否为 \(1\),若为\(1\),将 \(srcValue\) 左移一位后返回(此处不需要真正的左移,因为不需要保留左移之后高位的结果,所以直接进行截位操作再拼接零即可),同时返回相应的指数,以此类推。这样用一个优先级左移器同时做了 \(lzd+\) 左移两步操作,并且还产生了对应的 \(Ef32\),省掉根据 \(lzd\) 计算 \(Ef32\) 指数,从而实现了 \(f16\) 是非规格数的情况下转换成 \(f32\) 的快速算法。\(f32\) 转换成 \(f64\) 采用的算法类似,不再赘述。

向量浮点融合乘加算法

浮点融合乘加计算 \(fpa × fp\_b + fp\_c\),中间的乘法计算 \(fpa × fp\_b\) 就像没有范围和精度限制一样,不进行舍入,最终只舍入一次到目标格式。FMA通常作采用流水线实现,步骤主要包括乘法、加法、归一化移位和舍入。 本章介绍向量浮点融合乘加算法,其功能包括:

(1)\(1\)\(fp64 = fp64 × fp64 + fp64\)

(2)\(2\)\(fp32 = fp32 × fp32 + fp32\)

(3)\(4\)\(fp16 = fp16 × fp16 + fp16\)

(4)\(2\)\(fp32 = fp16 × fp16 + fp32\)

(5)\(1\)\(fp64 = fp32 × fp32 + fp64\)

\(1\))(\(2\))(\(3\))中的源操作数和目的操作数都是同一种浮点数格式,(\(4\))(\(5\))中两个乘数的宽度相同,另一个加数和结果的宽度相同并且是乘数宽度的二倍。

标量单一精度格式算法

计算流程是先计算两个浮点数相乘的不舍入的结果,再用这个不舍入的乘积和第三个数相加。算法流程图如图,用公式 \(fp\_result = fp\_a × fp\_b + fp\_c\) 表达计算过程,图中 \(Sa、Sb、Sc\) 分别是 \(fp\_a、fp\_b、fp\_c\) 的有效数字,\(Ea、Eb、Ec\) 则是 \(fp\_a、fp\_b、fp\_c\) 的指数:

FMA 算法流程图

为了下文描述方便,定义一些参数,参数含义及取值如表:

参数含义及不同精度下的取值
参数 \(f16\) \(f32\) \(f64\) 含义
\(significandWidth\) \(11\) \(24\) \(53\) 有效数字宽度
\(exponentWidth\) \(5\) \(8\) \(11\) 指数宽度
\(rshiftBasic\) \(14\) \(27\) \(56\) \(fp\_c\) 有效数字与乘积有效数字对齐需右移的位数
\(rshiftMax\) \(37\) \(76\) \(163\) \(fp\_c\) 有效数字最大的右移位数(超过这个值 \(g\)\(r\) 都是 \(0\)
无符号整数乘法

两个浮点数相乘规则是符号位相乘、指数位相加(不是单纯相加,需要考虑偏置)、有效数字(包括隐含位和尾数位)相乘。有效数字乘法实际上是定点数乘法,其和无符号整数乘法原理相同。

二进制竖式乘法是原始的乘法算法,\(n\) 比特 \(C=A×B\) 竖式法如图。此过程会产生 \(n\) 个部分积,并将这些部分积错位相加。

二进制竖式法计算乘法

竖式法的乘法算法延时很大,针对乘法运算的优化,主要集中在两个方面:一是减少部分积的个数(比如 \(Booth\) 编码),二是减少加法器带来的延时(比如 \(CSA\) 压缩)。

在计算两个浮点相乘时,会将它们的有效数字相乘,有效数字是无符号位的,所以使用无符号整数乘法就可以计算两个有效数字相乘。无符号整数乘法实现的算法有很多种,下面对比三种算法。

方式一:直接使用乘号 \(×\),由综合工具自己决定。

方式二:使用类似手算十进制乘法的竖式法,两个 \(n\) 位数相乘会产生 \(n\) 个部分积,再对 \(n\) 个部分积进行 \(CSA\) 压缩(后面介绍),压缩成两个数相加。

方式三:使用 \(Booth\) 编码,产生 \((n+1)/2\) 向上取整个部分积,再通过 \(CSA\) 压缩成两个数相加。

表中的数据是使用 \(TSMC7nm\) 工艺库对两个 \(53\) 比特无符号整数相乘(对于 \(f64\) )的结果。目标频率为 \(3GHz\),理论每周期时间 \(333.33ps\),但是需要考虑 \(clock \quad uncertain\) 和工艺角偏差问题,给后端留设计余量,每周期可用时间约为 \(280ps\),所以显然一周期内做不完乘法,实际中求隐含位还需要一定的时间,所以更加无法在一周期内实现 \(53bit\) 相乘。因此虽然方式一面积更小延时更短一些,但是因为方式一无法进行流水线设计,也只能选择方式二或者方式三,方式三相较与方式二延时更短面积更小,所以选方式三作为无符号整数乘法的实现方式。

无符号整数乘法三种算法比较
算法 延时(\(ps\) 面积(\(um²\) 是否可流水
方式一 \(285.15\) \(1458.95\)
方式二 \(320.41\) \(2426.34\)
方式三 \(302.19\) \(2042.46\)
Booth 编码

\(Booth\) 编码目的是减少乘法器部分积的个数,以二进制无符号数整数乘法 \(C=A*B\) 为例推导 \(Booth\) 编码算法。

下式是二进制无符号整数通用的表达形式,为了后续变换,在头尾各加上一个 \(0\),其值不变。

\[ B = 2^{n-1}B_{n-1} + 2^{n-2}B_{n-2} + \ldots + 2B_1 + B_0 + B_{-1}, \quad B_{-1}=0 \]

等价变换后,相邻两比特 \(1\) 会被抵消为 \(0\)。如果是连续 \(1\),则最低位的 \(1\) 会变成 \(-1\),最高位 \(1\) 的上一位由 \(0\) 变成 \(1\),其中 \(1\) 全部变成 \(0\),这种变换就是布斯变换。布斯变换能对连续 \(1\) 的数量大于等于 \(3\) 起到化简作用,连续 \(1\) 的位数越多,化简效果越好。但上述变换并不能在硬件电路中起到优化作用,因为并不能保证某个部分积恒为 \(0\)。所以在电路设计中,一般采用改进的布斯编码方式,能真正减少部分积的数量。

\[ \begin{split} B &= 2^{n-1}B_{n-1} + 2^{n-2}B_{n-2} + \ldots + 2B_1 + B_0 + B_{-1} \\ &= 2^{n-1}B_{n-1} + 2^{n-2}B_{n-2} + 2^{n-2}B_{n-2} - 2^{n-2}B_{n-2} + \ldots + 2B_1 + 2B_1 - 2B_1 + B_0 + B_0 - B_0 + B_{-1} \\ &= 2^{n-1}(B_{n-1}+B_{n-2}) + 2^{n-2}(-B_{n-2} + B_{n-3}) + \ldots + 2(-B_1 + B_0) + (-B_0 + B_{-1}) \end{split} \]

再次进行等价变换,不过这次对 \(n\) 加一些约束,假设 \(n\) 为奇数,依旧在最后一位补一个零,补零之后长度变为偶数,再在最高位前面补一个零,将总长度变为 \(n+2\),这样做的目的是方便后面推导。

\[ \begin{split} B &= 2^nB_n + 2^{n-1}B_{n-1} + 2^{n-2}B_{n-2} + \ldots + 2B_1 + B_0 + B_{-1} \\ &= -2 × 2^{n-1}B_n + 2^{n-1}B_{n-1} + 2^{n-2}B_{n-2} + 2^{n-2}B_{n-2} - 2^{n-2}B_{n-2} + \ldots \\ &\quad + 2^3B_3 + 2^3B_3 - 2^3B_3 + 2^2B_2 + 2B_1 + 2B_1 - 2B_1 + B_0 + B_{-1} \\ &= 2^{n-1}(-2B_n + B_{n-1} + B_{n-2}) + 2^{n-2}(-2B_{n-1} + B_{n-2} + B_{n-3}) + \ldots \\ &\quad + 2^2(-2B_3 + B_2 + B_1) + (-2B_1 + B_0 + B_{-1}) \end{split} \]

通过等价变形后可以发现,多项表达式的项数变成了\((n+1)/2\)\(n\) 为奇数),如果 \(n\) 为偶数需要在尾部补一个零,最高位前面补两个零,多项表达式的项数变成了 \(n/2+1\)\(n\) 为偶数),综合奇偶数的情况,多项表达式的项数为\((n+1)/2\) 向上取整。原二进制数从 \(LSB\) 开始,以三位为一组(第一组最低位需补一个附加位 \(0\)即,最高位根据 \(n\) 的奇偶,奇数补一个 \(0\),偶数补两个 \(0\),目的是让补完 \(0\) 后的长度是奇数),相邻两组之间重叠一位(低位组的最高位与高位组的最低位重叠),构成新的多项式因子,这就是改进的布斯编码方式。

两个二进制数相乘,如果对乘数进行改进型布斯编码,则得出的部分积个数可以减少一半。记被乘数为 \(A\),乘数为 \(B\)\(B_{2i+1}\)\(B_{2i}\)\(B_{2i-1}\),分别为 \(X\) 的连续三位,其中 \(i\) 为自然数 \(N\)\(PP_i\)\(i\) 不同取值下对应的部分积,对 \(B\) 行改进的布斯变换后再与 \(A\) 乘,\(Booth\) 编码与 \(PP\) 真值表如表中所示。

\(Booth\) 编码与 \(PP\) 真值表
\(B_{2i+1}\) \(B_{2i}\) \(B_{2i-1}\) \(PP_i\)
\(0\) \(0\) \(0\) \(0\)
\(0\) \(0\) \(1\) \(A\)
\(0\) \(1\) \(0\) \(A\)
\(0\) \(1\) \(1\) \(2A\)
\(1\) \(0\) \(0\) \(-2A\)
\(1\) \(0\) \(1\) \(-A\)
\(1\) \(1\) \(0\) \(-A\)
\(1\) \(1\) \(1\) \(0\)

根据乘数每连续三位的值,求出其对应的部分积,减少了一半部分积数量,就好像将乘数看作是四进制数进行乘法,因此被称为基 \(4\) 布斯编码。采用基 \(4\) 布斯编码的乘法相较于传统乘法运算,优化效果很明显且易于实现,可以满足大部分应用要求。

\(Booth\) 编码时需要计算五种部分积的情况,\(0\)\(A\)\(2A\)\(-A\)\(-2A\)\(0\)\(A\) 无需计算,\(2A\) 左移一位即可,\(-A\)\(-2A\) 需要取反加一的操作,本文介绍一种快速处理取反加一的算法。

为介绍原理简单,我们假设计算 \(f16\),有效数字是 \(11\) 位,会生成 \(6\) 个部分积,部分积的宽度是 \(22\) 位的,如图所示,图中彩色位置宽度是 \(12\) 位的,表示 \(A\) 可能乘 \(0\) 、乘 \(1\) 或者乘 \(2\)。因为最后一个部分积三比特的编码是 \(0\)xx,所以其值不可能是负数,我们假设除了最后一个部分积外其它部分积都是负数,则需要对其余每个部分积取反加一,彩色部分表示的是仅取反后的结果,我们把当前部分积加的一放到下一个部分积对应的位置,这样保证了部分积的和不变,并且避免了对当前部分积加一产生一个进位链的问题,而最后一个部分积是非负的,不需要处理它的加一。

假设部分积全为负数取反加一示意图

上图中的 \(1\) 可以先进行求和化简,得到下图的结果。

假设部分积全为负数化简后的结果

如果实际部分积的值是正数,那么需要对以上结果进行修正,也就是在彩色左边一比特位置加一,并且把下一个部分积尾部加的一变成零。如图所示,\(Si\)\(i\)\(0\) 计)表示第 \(i\) 个部分积的符号位,这样就变成了通用的形式,彩色位置只计算\(0\)\(A\)\(2A\)\(\sim A\)\(\sim 2A\),加快部分积生成速度。

部分积为正数时修正结果

这里还有一点需要说明,部分积的和是乘积的结果,但是部分积求和的时候也可能会进位,这个进位对于乘法来说是无意义的,但是当乘积和更宽的数字相加的时候就会错误进位,修正方式是部分积最高位再增加一位,如图所示。

部分积进位修正

这样就可以保证所有部分积加起来之后进位是正确的,到此 \(Booth\) 编码介绍结束,注意是以 \(11\) 比特乘法为例介绍的,\(f16\)\(f64\) 有效数字位数为奇数,但是 \(f32\) 有效数字位数为偶数,最高位补零需要稍作区别,其他过程类似,不再赘述。

CSA 压缩

\(Carry\)-\(Save\)-\(Adder\) 保留进位加法器,作用是将 \(n\) 个加数通过某些逻辑压缩成 \(m\) 个加数(\(m<n\)),典型的是 \(3\_2\) 压缩和 \(4\_2\) 压缩。

假设计算两个二进制数相加 \(A+B\),它们的和与进位真值表,表中 \(A[i]+B[i]\) 是十进制结果,也是 \(A[i]\)\(B[i]\)\(1\) 的数量:

\(A+B\) 和与进位真值表
\(A[i]\) \(B[i]\) \(A[i] + B[i]\) \(Sum[i]\) \(Car[i]\)
\(0\) \(0\) \(0\) \(0\) \(0\)
\(0\) \(1\) \(1\) \(1\) \(0\)
\(1\) \(0\) \(1\) \(1\) \(0\)
\(1\) \(1\) \(2\) \(0\) \(1\)

化简成逻辑表达式如下:

\(Sum = A\) ^ \(B\)

\(Car = A\) & \(B\)

\(Result = A+B = Sum + (Car << 1)\)

三个数相加,和是两个数异或操作,进位是两个数同为 \(1\)\((Car << 1)\) 是因为当前比特的进位是进到下一比特去的,这里只是做一下推导方便后面理解,实际中两个加数产生和与进位对加法没有加速效果。

假设我们要计算三个数的和 \(A+B+C\)\(CSA\) 关键是产生和与进位,真值表如表所示:

\(A+B+C\) 和与进位真值表
\(A[i]\) \(B[i]\) \(C[i]\) \(A[i] + B[i] + C[i]\) \(Sum[i]\) \(Car[i]\)
\(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(0\) \(0\) \(1\) \(1\) \(1\) \(0\)
\(0\) \(1\) \(0\) \(1\) \(1\) \(0\)
\(0\) \(1\) \(1\) \(2\) \(0\) \(1\)
\(1\) \(0\) \(0\) \(1\) \(1\) \(0\)
\(1\) \(0\) \(1\) \(2\) \(0\) \(1\)
\(1\) \(1\) \(0\) \(2\) \(0\) \(1\)
\(1\) \(1\) \(1\) \(3\) \(1\) \(1\)

从上表中可以看出一些规律,产生 \(Sum[i]\)\(Car[i]\) 其实只和 \(A[i]+B[i]+C[i]\) 的和有关,即 \(A[i]\)\(B[i]\)\(C[i]\)\(1\) 的个数,化简之后的表达式如下:

\(Sum = A\) ^ \(B\) ^ \(C\)

\(Car = (A\) & \(B) \quad | \quad (A\) & \(C) \quad | \quad (B\) & \(C)\)

\(Result = A+B+C = Sum + (Car << 1)\)

三个数相加,和是三个数异或操作,进位是至少有两个数为 \(1\)\((Car << 1)\) 是因为当前比特的进位是进到下一比特去的。这样通过两级异或门的延迟就可以把三个数相加转化成两个数相加,这种方式节省了很多时间,尤其是在位宽比较长的情况下。

四个数相加情况复杂一些,因为四个数同时为 \(1\) 时,总和为 \(4\),需要进位 \(2\),我们把其中一个进位叫做 \(Cout\),另一个进位叫 \(Car\),然后把当前四比特相加产生的 \(Cout\) 传到下一级叫做 \(Cin\)\(Cin\) 和四个数变成五个数相加,这样操作之后每一比特输入就变成了五个数,\(A[i]\)\(B[i]\)\(C[i]\)\(D[i]\)\(Cin[i]\),输出三个数 \(Sum[i]\)\(Cout[i]\)\(Car[i]\),最低位的 \(Cin[0]\)\(0\),其他位的 \(Cin[i]\) 是低一比特的 \(Cout[i-1]\),如表所示。

\(A+B+C+D\) 和与进位真值表
\(A[i]+B[i]+C[i]+D[i]+Cin[i]\) \(Sum[i]\) \(Cout[i]\) \(Car[i]\)
\(0\) \(0\) \(0\) \(0\)
\(1\) \(1\) \(0\) \(0\)
\(2\) \(0\) \(1/0\) \(0/1\)
\(3\) \(1\) \(1/0\) \(0/1\)
\(4\) \(0\) \(1\) \(1\)
\(5\) \(1\) \(1\) \(1\)

这个真值表的化简方式有很多种,下面介绍一种可行的方式,\(Sum[i]\) 的值容易得出是五个输入的异或,\(Sum[i] = A[i]\)^\(B[i]\)^\(C[i]\)^\(D[i]\)^\(Cin[i]\)\(Car[i]\)\(Cout[i]\) 较复杂,我们规定 \(Cout[i]\) 只由前三个数产生,即前三个数的和大于 \(1\) 时,\(Cout[i] = 1\),表是 \(Cout[i]\) 真值表:

\(A+B+C\) 产生 \(Cout\) 真值表
\(A[i]+B[i]+C[i]\) \(Cout[i]\)
\(0\) \(0\)
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(1\)

\(Cout[i]\) 可表示为:\(Cout[i] = (A[i]\)^\(B[i])?C[i]:A[i]\)\(Car[i]\) 则由 \(D[i]\)\(Cin[i]\) 产生,表是 \(Car[i]\) 的真值表。

\(A+B+C\) 产生 \(Car\) 真值表
\(A[i]+B[i]+C[i]+D[i]\) \(Car[i]\)
\(0\) \(D[i]\)
\(1\) \(Cin[i]\)
\(2\) \(D[i]\)
\(3\) \(Cin[i]\)

\(Car[i]\) 可表示为:\(Car[i] = (A[i]\) ^ \(B[i]\) ^ \(C[i]\) ^ \(D[i]) ? Cin[i] : D[i]\),具体来看 \((A[i]\) ^ \(B[i]\) ^ \(C[i]\) ^ \(D[i]) = 1\)时,\(A[i]+B[i]+C[i]+D[i] = 1/3\),此时 \(Cin[i] = 1\) 会产生进位,\((A[i]\) ^ \(B[i]\) ^ \(C[i]\) ^ \(D[i]) = 0\) 时,\(A[i]+B[i]+C[i]+D[i] = 0/4\),此时 \(D[i]\) 等于 \(0\) 表示 \(A[i]+B[i]+C[i]+D[i] = 0\),再加 \(Cin\) 也不产生进位,\(D[i]\) 等于 \(1\) 表示 \(A[i]+B[i]+C[i]+D[i] = 4\),再加 \(Cin\) 也会产生进位,所以综合以上推导过程, \(CSA4\_2\) 的表达式如下:

\(Sum[i] = A[i]\) ^ \(B[i]\) ^ \(C[i]\) ^ D[i]$ ^ \(Cin[i],Cin[i] = Cout[i-1],Cin[0] = 0\)

\(Cout[i] = (A[i]\) ^ \(B[i])?C[i]:A[i]\)

\(Car[i] = (A[i]\) ^ \(B[i]\) ^ \(C[i]\) ^ \(D[i])?Cin[i]:D[i]\)

\(Result = A+B+C+D = Sum + (Car << 1)\)

使用\(TSMC7nm\)工艺库对不同输入异或门、\(CSA3\_2\)\(CSA4\_2\) 分别进行综合对比延时和面积,不同输入异或门综合结果见表。

不同输入异或门综合结果
\(106bit\) 延时(\(ps\) 面积(\(um²\)
\(A\)^\(B\) \(13.74\) \(38.66880\)
\(A\)^\(B\)^\(C\) \(23.01\) \(63.09120\)
\(A\)^\(B\)^\(C\)^\(D\) \(24.69\) \(87.51360\)
\(A\)^\(B\)^\(C\)^\(D\)^\(E\) \(37.21\) \(99.72480\)

\(CSA3\_2、CSA4\_2\) 综合结果见表。

\(CSA3\_2、CSA4\_2\) 综合结果
\(106bit\) 延时(\(ps\) 面积(\(um²\)
\(CSA3\_2\) \(23.23\) \(104.42880\)
\(CSA4\_2\) \(40.63\) \(237.86881\)

可见 \(CSA4\_2\) 虽说理论上是三级异或门的延迟,\(CSA3\_2\) 理论上是两级异或门的延迟,但是真正物理实现之后,\(CSA4\_2\) 只是比两级 \(CSA3\_2\) 快一点点,所以尽量用 \(CSA3\_2\),除非一级 \(CSA4\_2\) 可以替代两级 \(CSA3\_2\) 的情况,比如 \(4->2\) 压缩,\(8->2\) 压缩。

CSAn_2

两个无符号整数乘法使用 \(Booth\) 编码产生的部分积数量为 \(\frac{n+1}{2}\) 向上取整,因为部分积压缩之后要和位宽更大的数相加,为保证进位正确,部分积位宽拓展一比特,各数据格式部分积数量及位宽见表。

不同数据格式部分积数量及位宽
数据格式 有效数字位数 部分积数量 部分积位宽
\(fp16\) \(11\) \(6\) \(12\)
\(fp32\) \(24\) \(13\) \(25\)
\(fp64\) \(53\) \(27\) \(54\)

按照尽量用 \(CSA3\_2\),除非一级 \(CSA4\_2\) 可以替代两级 \(CSA3\_2\) 的原则,各数据格式所用 \(CSA3\_2、CSA4\_2\) 级数如表。

不同数据格式部分积 \(CSA\) 压缩过程
数据格式 \(CSA3\_2\) 级数 \(CSA4\_2\) 级数 过程(\(->\)表示 \(CSA3\_2\),\(-->\)表示 \(CSA4\_2\))
\(fp16\) \(1\) \(1\) \(6->4-->2\)
\(fp32\) \(3\) \(1\) \(13->9->6->4-->2\)
\(fp64\) \(3\) \(2\) \(27->18->12->8-->4-->2\)
指数处理和右移

如果按照常规做法,\(fp\_a\)\(fp\_b\) 相乘结果的指数和 \(fp\_c\) 的指数结果大小关系未知,那么就像计算浮点加法那样需要对其中指数小的进行右移,那么 \(fp\_a\)\(fp\_b\) 相乘结果的尾数和 \(fp\_c\) 的尾数都可能右移,这样设计会造成使用两个移位器,增大了面积,并且需要等 \(fp\_a\)\(fp\_b\) 相乘结果计算出来再对它的有效数字右移,增大了电路的延迟,现介绍一种算法避免使用两个移位器,并且可以和 \(fp\_a\)\(fp\_b\) 相乘并行起来减小电路的延迟。

首先指数位是当作无符号数处理的,但是它和真实指数之间有一个指数偏置,同时还要考虑 \(denormal\) 的情况,记 \(E\_fix\) 是处理 \(denormal\) 情况之后的指数位,记 \(E\_bit\) 是原始的指数位,当 \(E\_bit\) 所有位是 \(0\)\(E\_fix = 1\),其他情况下 \(E\_fix = E\_bit\)

\[ E\_real = E\_fix - bias, \quad bias = (1 << (exponentWidth - 1)) - 1 \]

上式中真正的指数 \(E\_real\) 等于 \(E\_fix\) 减去一个偏置值 \(bias\)\(exponentWidth\)\(E\_bit\) 的宽度,\(bias\) 的值等于 \(E\_bit\) 最高位是 \(0\) 其余位全是 \(1\)。在不考虑有效数字乘积的进位或借位的情况下,\(fp\_a\)\(fp\_b\) 相乘的真实指数结果Eab_real如下式所示:

\[ Ea\_real = Ea\_fix + Eb\_fix - 2 × bias \]

\(fp\_a\)\(fp\_b\) 相乘的二进制指数结果 \(Eab\_bit\) 的计算公式见下式:

\[ Eab\_bit = Cat(0.U, Ea\_fix + \&Eb\_fix).asSInt - bias.S \]

其中 \(+\)& 的操作将 \(Ea\_fix+Eb\_fix\) 的结果拓展一位保留进位,保留进位的原因是因为后面还要减去一个偏置值,不保留进位结果就不对了,还要考虑减去偏置值可能出现负数的情况,所以再拓展一位,即在最高位拼一比特 \(0\),最后再减去偏置值 \(bias\),这样就得到了在不考虑有效数字乘积的进位或借位的情况下,\(fp\_a\)\(fp\_b\) 相乘的二进制指数结果 \(Eab\_bit\)。 然后我们构造一个指数 \(Eab\),其值如下式:

\[ Eab = Eab\_bit + rshiftBasic.S \]

并且认为 \(fp\_a\)\(fp\_b\) 相乘的二进制指数结果为 \(Eab\),为了保证无损精度的 \(fp\_a\)\(fp\_b\) 相乘的有效数字和 \(fp\_c\) 的有效数字相加,我们分别将这两个加数拓展位宽,对于 \(fp\_c\) 的有效数字拓展成 \(3×significandWidth+4\),位分布情况见图,其中 \(g0,r0,g1,r1\) 是用来保存右移过程中 \(gound,round\) 位的:

fp_c有效数字扩展位分布情况

如上图所示,\(fp\_c\) 有效数字比 \(fp\_a\)\(fp\_b\) 有效数字乘积高 \(significandWidth+2\) 位,因为乘积结果小数点前面有两位,所以按照乘积结果是 \(1\).xxx对齐,高 \(significandWidth+3\) 位,这就是 \(rshiftBasic = significandWidth+3\) 的原因。

\(fp\_c\_significand\_cat0 = Cat(fp\_c\_significand,0.U(2×significandWidth+4))\)\(fp\_c\_significand\)\(fp\_c\) 的有效数字。如果 \(Ec\_fix = Eab = Eab\_bit + rshiftBasic.S\)\(fp\_c\_significand\_cat0\) 正好比 \(Eab\_bit\)\(significandWidth+3\)\(fp\_c\_significand\_cat0\) 不用右移就对齐了;如果 \(Ec\_fix > Eab\),理论上讲 \(fp\_c\_significand\_cat0\) 需要左移,但是因为中间有 \(g0,g1\) 做间隔,并且低比特是不会产生进位的,低比特只会影响舍入结果,所以实际不需要对 \(fp\_c\_significand\_cat0\) 左移;如果 \(Ec\_fix < Eab\),此时就要对 \(fp\_c\_significand\_cat0\) 进行右移了,右移的位数 \(rshift\_value = Eab - Cat(0.U,Ec\_fix).asSInt\)\(rshift\_value\) 的是来自多个数的加法,所以最低位先计算好,所以再右移时要先用 \(rshift\_value\) 最低位当作 \(Mux\) 的选择信号,再依次用高位当作 \(Mux\) 的选择信号,右移过程需要计算 \(gound、round、sticky\)(简称 \(grs\)),对于 \(gound\)\(round\),已经在拓展位宽时保留了该位置,无需额外计算,对于 \(sticky\) 有两种方法,方法一是再拓展一些位宽用来存放右移出去的位,等全部右移结束之后再进行求 \(sticky\),方法二是在右移过程中根据 \(Mux\) 选择信号的值同时计算出 \(sticky\),方法二比方法一延时更小。下面是方法二的设计代码:

/**
 * 使用Mux进行移位,先用最低位,输出位宽为srcValue + 1(Sticky)
 */
def shiftRightWithMuxSticky(srcValue: UInt, shiftValue: UInt): UInt = {
  val vecLength  = shiftValue.getWidth + 1
  val res_vec    = Wire(Vec(vecLength,UInt(srcValue.getWidth.W)))
  val sticky_vec = Wire(Vec(vecLength,UInt(1.W)))
  res_vec(0)    := srcValue
  sticky_vec(0) := 0.U
  for (i <- 0 until shiftValue.getWidth) {
    res_vec(i+1) := Mux(shiftValue(i), res_vec(i) >> (1<<i), res_vec(i))
    sticky_vec(i+1) := Mux(shiftValue(i), sticky_vec(i) | res_vec(i)((1<<i)-1,0).orR,
    sticky_vec(i))
  }
  Cat(res_vec(vecLength-1),sticky_vec(vecLength-1))
}

在右移的时候还有一个加快速度的方法,\(rshift\_value\) 的位宽是 \(exponentWidth+1\),而 \(fp\_c\_significand\_cat0\) 宽度是\(3*significandWidth+4\)\(rshift\_value\) 中可能会有溢出的位宽,比如用一个位宽是 \(5\) 的数去右移一个宽是 \(7\) 的数,\(a(6,0) >> b(4,0)\)\(b\) 中的第三位最大值是 \(7\),已经够了 \(a\) 的位宽,所以 \(b\) 的高两位只要有非零数,\(a\) 的右移结果就是零,右移结果可简化为 \(Mux(b(4,3).orR,0.U, a(6,0) >> b(2,0))\),下表列出三种浮点数据格式使用的 \(rshift\_value\) 位宽。

不同浮点格式使用的 \(rshift\_value\) 位宽
数据格式 \(fp\_c\_significand\_cat0\) 位宽 \(rshift\_value\) 位宽 使用的位宽
\(f16\) \(37\) \(6\) \(6\)
\(f32\) \(76\) \(9\) \(7\)
\(f64\) \(163\) \(12\) \(8\)

根据 \(rshift\_value\) 的值分为三种情况,\(rshift\_value <= 0\) 此时不需要右移,\(sticky\) 结果是 \(0\)\(rshift\_value > rshiftMax\),右移结果是 \(0\)\(sticky\) 结果是 \(fp\_c\_significand\_cat0\) 或缩减;\(0 < rshift\_value <= rshiftMax\),右移结果和 \(sticky\)\(shiftRightWithMuxSticky\) 计算的结果。

至此,本小节介绍了指数处理的方法,右移器设计,右移过程中 \(grs\) 的处理。

有效数字相加

\(fp\_c\) 有效数字右移之后的结果 \(rshift\_result\) 要和 \(CSAn\_2\) 压缩的两个结果进行相加,由于 \(fp\_c\)\(fp\_a\) 乘以 \(fp\_b\) 的符号可能相反,当相反时要做减法,并且减法结果可能是负数,为了判断正负需要再补充一位符号位,\(fp\_c\_rshiftValue\_inv\) 根据符号是否相反选择 \(rshift\_result\)(符号位补 \(0\))或者 \(rshift\_result\) 取反(符号位补 \(1\)),所以要用 \(fp\_c\_rshiftValue\_inv\)\(CSAn\_2\) 压缩的两个结果进行相加,不过在做减法的时候 \(fp\_c\_rshiftValue\_inv\) 只是对 \(rshift\_result\) 取反,还需要在右移 \(grs\) 都为 \(0\) 的情况下对最低位 \(+1\),把这一个 \(+1\) 放到 \(CSAn\_2\) 压缩的两个结果中的 \(carry\) 位,因为 \(carry\) 位恒为 \(0\),直接放到这一位可以节省加法器的使用,节省面积。这三个数位宽不同,\(fp\_c\) 有效数字右移之后的结果位宽 \(3×significandWidth+4\)\(CSAn\_2\) 压缩的两个结果位宽是\(2×significandWidth+1\)\(+1\) 的原因是之前提到的部分积要拓展一位保证进位正确。计算三个不同位宽的和的策略是,先将 \(CSAn\_2\) 压缩的两个结果的低 \(2×significandWidth+1\) 位和 \(rshift\_result\)\(2×significandWidth\) 位(最高位拼一比特 \(0\) 凑成 \(2×significandWidth+1\) 位)进行一次 \(CSA3\_2\) 压缩之后,再对 \(CSA3\_2\) 压缩的两个结果相加,记作 \(adder\_low\_bits\),同时计算 \(rshift\_result\)\(significandWidth+4\)\(+1\),然后根据低 \(2×significandWidth+1\) 位相加的结果最高位是否为 \(1\) 来选择 \(fp\_c\_rshiftValue\_inv\)\(significandWidth+4\) 位或者 \(fp\_c\_rshiftValue\_inv\)\(significandWidth+4\)\(+1\),记作 \(adder\_high\_bits\)

还需要考虑减法时对右移 \(grs\) 取反加一,最终有效数字的加法结果 \(adder\)(含右移 \(grs\))构成是:\(adder\_high\_bits\)\(adder\_low\_bits\)、右移 \(grs\)(减法时取反加一)。因为 \(adder\) 可能是负数,拓展了 \(1bit\) 仅用于 \(adder\) 的符号判断, 之后不需要使用,\(adder\_inv\)\(adder\) 是负数时进行取反并且去掉了该符号判断位。

LZD、左移、舍入与未舍入尾数结果

在计算完 \(adder\_inv\) 后,需要对 \(adder\_inv\) 进行前导零检测,以确定需要左移的位数,进而对尾数结果规格化和舍入。

\(adder\_inv\)\(LZD\) 时有一个指数限位的问题,记 \(E\_greater\)\(Eab\)\(fp\_a\)\(fp\_b\) 相乘的指数结果),左移的值不能比 \(E\_greater\) 更大,因为等于 \(E\_greater\) 时,指数结果已经全为 \(0\) 了。为了解决这个问题,同浮点加法器一样在左移时使用 \(mask\) 进行限位。

对于 \(adder\) 是负数的情况,\(-adder\) 应该是 \(adder\) 取反 \(+1\),由于 \(+1\) 会产生长进位链,只做取反操作,然后计算 \(adder\_inv\)\(LZD\),此时可能会出现一位的偏差,当 \(adder\) 取反之后末尾全是连续 \(1\) 时,此时再加 \(1\) 最高位的 \(1\) 就要产生进位了,解决一比特偏差的方法是对 \(adder\) 做一个尾随零检测(\(TZD\)),当 \(LZD + TZD = adder\) 的宽度时,\(adder\) 取反之后末尾全是连续 \(1\),这种情况下需要对左移结果修正。左移修正之后得到未舍入的结果,对未舍入的结果 \(+1\) 得到舍入后的结果。

最终结果

根据 \(adder\) 的正负得到符号位结果,\(grs\) 的计算需要同时结合第五步右移和第七步左移过程。舍入策略采用 \(after \quad rounding\),为了判断 \(underflow\),需要再使用一组专门用来判断 \(underflow\)\(grs\)。根据舍入模式和 \(grs\) 得到是否需要舍入,选择出尾数结果,指数结果根据舍入情况得到。

输入操作数含特殊值,如 \(NaN\),无穷,零时,结果单独计算,并根据实际输入数的情况从特殊结果和正常结果中选择一个,标志位结果除了除零标志位外,其他四种都可产生。

向量单一精度格式算法

向量操作主要设计思路是在时序满足要求的情况下共用硬件。

\(Booth\) 编码过程中,\(f16\) 产生 \(6\)\(pp\)\(f32\) 产生 \(13\)\(pp\)\(f64\) 产生 \(27\)\(pp\),所以 \(Booth\) 编码时 \(f64\) 产生的 \(27\)\(pp\) 的位置可以放两个 \(f32\) 产生的 \(13\)\(pp\),同理 \(f32\) 产生的 \(13\)\(pp\) 的位置可以放两个 \(f16\) 产生的 \(6\)\(pp\),这样就可以继续共用一套 \(CSA\_27to2\) 压缩,向量共用 \(Booth\) 编码见图。

向量共用Booth编码示意图

\(fp\_c\) 尾数右移时,\(f64\)\(f32\) 中的其中一个尾数右移可以共用一个右移器,其他的右移器都是独立的。

\(CSA\_3to2\) 也是公用的,第三个操作数来自 \(fp\_c\) 尾数右移的结果,将 \(2\)\(f32\)\(4\)\(f16\) 尾数右移的结果拼接起来,和共用的 \(Booth\) 编码压缩之后的两个操作数一起进行 \(3\_2\) 压缩。

压缩完之后的加法器也是共用的,通过不同格式分配不同的位,并按位隔开使低位进位不影响高位结果。

\(LZD,TZD\),左移器共用思路同右移器,\(f64\)\(f32\) 中的一个是公用的,其他独立。

向量混合精度格式算法

向量混合精度格式计算有两种:

(1) \(2\)\(fp32 = fp16 × fp16 + fp32\);

(2) \(1\)\(fp64 = fp32 × fp32 + fp64\)

对于两个相同宽度的乘数,本质还是指数相加有效数字相乘,不用向浮点加法那样先把它们格式转换成和结果相同的格式,只需简单拓展位宽即可,指数高位补零,尾数低位补零,和高精度的浮点操作数对齐。对齐后,按照单一精度格式进行计算即可。

向量浮点除法算法

除法是现代处理器中最具代表性的浮点函数之一。在硬件中计算除法存在两个主要的算法:具有线性收敛性并基于减法的数字迭代算法,以及基于乘法并具有二次收敛性的乘法算法,基于减法的数字迭代算法能效更高,所需面积更小。本文后续的数字迭代都是指基于减法的数字迭代。对于常用的浮点精度,双精度、单精度和半精度、数字迭代方法要快得多。在数字迭代除法中,最关键的一点是商数位的选择,每次迭代,获得商的一位数。要实现独立于除数的简单 \(Radix-4\) 选择函数,除数需要调整到足够接近1的值。该缩放在数字迭代之前执行。

数字迭代算法由于在性能、面积和功耗方面具有很好的折衷性而被广泛应用于的高性能处理器中,本文基于 \(SRT\) 除法(\(Sweeney-Robertson-Tocher Division\)),采用 \(Radix-64\) 的浮点除法算法,每周期计算 \(6\) 比特商。为了减小开销,每次 \(Radix-64\) 迭代由三次 \(Radix-4\) 迭代组成;在连续的 \(Radix-4\) 迭代之间使用推测算法减少延时。

标量浮点除法算法

本文实现的 \(Radix-64\) 标量浮点除法算法,当输入操作数和结果都是规格化数时,对于双精度、单精度、半精度浮点除法,有较低的延迟,分别为 \(11、6、4\) 周期延迟,这些延迟包括缩放和舍入的周期。在输入操作数或结果中有非规格化数的情况下,需要一个或两个额外的规格化周期。

指数结果很容易就能得出,重点是有效数字的除法,有效数字除法器执行被除数有效数字 \(x\) 和除数有效数字 \(d\) 的浮点除法,以获得有效数字的商 \(q = x/d\)。两个操作数需要是规格化数,\(x,d ∈ [1,2)\),非规格化的操作数也是允许的,在数字迭代之前,对非正规化操作数进行规格化。如果两个操作数在 \([1,2)\) 中规格化,则结果在 \([0. 5,2)\)中;这样,需要商的最低有效位(\(LSB\))右边的两位用于舍入,即保护位和舍入位。

当结果被归一化时,保护位用于舍入,\(q ∈ [1,2)\),当结果未被归一化时,舍入位用于舍入,\(q ∈ [0.5,1)\)。在后一种情况下,结果左移 \(1\) 位,保护位和舍入位分别成为归一化结果的 \(LSB\) 和保护位。为了简化舍入,结果被强制为 \(q ∈ [1,2)\)。注意只有当 \(x<d\)\(q<1\)。这种情况在早期被检测到,并将被除数左移 \(1\) 位,使得 \(q =2× x/d\)\(q ∈ [1,2)\),注意此时指数结果要进行相应调整。

用于除法的算法是 \(Radix-4\) 数字迭代算法,每个循环三次迭代,商的符号数字表示为数字集 {\(−2,−1,0,+1,+2\)};即基数 \(r =4\),数据集 \(a =2\)。每次迭代,通过选择函数获得商的一个数字。为了具有独立于除数的商位选择函数,除数必须缩放到接近 \(1\)。当然,为了保持结果的正确性,被除数需要按与除数相同的倍数缩放。

利用基\(-4\) 算法,每次迭代获得商的 \(2\) 比特。由于每个时钟周期执行三次基\(-4\) 迭代,因此每个周期获得 \(6\) 位商,这等效于 \(Radix-64\) 除法器。另外,注意到作为结果的整数位的第一商数位只能取值{\(+1,+2\)},并且其计算比其余数位的计算简单得多。把它和操作数预缩放并行计算,节省了一次单精度浮点数的迭代。另一方面,对于异常操作数有一种提前终止模式。当任何操作数为 \(NaN\)、无穷大或零时,或者在两个操作数均归一化的情况下除以 \(2\) 的次幂的情况下,引发提前终止。在后一种情况下,仅通过减小被除数的指数来获得结果。\(Radix-64\) 除法器的主要特征如下:

(1) 除数和被除数的预缩放。

(2) 第一个商数位与预缩放并行执行。

(3) 比较缩放后的被除数和除数,并将被除数左移,以得到\([1,2)\)的结果。

(4) 每个周期三次 \(Radix-4\) 迭代,每个周期 \(6\) 位。

(5) 支持半精度、单精度和双精度。

(6) 非规格化数支持,迭代前需要额外一周期将非规格化数规格化。

(7) 异常操作数提前终止。

数字迭代除法算法

数字迭代除法是一种迭代算法,每次迭代计算一个 \(radix-r\) 商数字 \(q_{i+1}\) 和一个余数。余数 \(rem[i]\) 用于获得下一个 \(radix-r\) 的数字。为了快速迭代,余数被保存在有符号数冗余表示的进位保存加法器中,本文选择了 \(radix-2\) 的有符号数表示余数,其中包含一个正数和一个负数。对于基数 \(r =4\),下式是第 \(i\) 位迭代之前的部分商的表达式

\[ Q[i] = \sum_{j=0}^i q_j × 4^{-j} \]

除数缩放到 \(1\) 附近之后的 \(radix-4\) 算法,商和余数由下式描述:

\[ q_{i+1} = SEL(\widehat{rem}[i]) \]
\[ rem[i + 1] = 4 × rem[i] - d × q_{i+1} \]

其中 \(\widehat{rem}[i]\) 是只具有几个比特的余数 \(rem[i]\) 的估计值。对于此算法,已确定仅需要余数的 \(6\) 个最高有效位(\(MSB\)),即 \(3\) 个整数位和 \(3\) 个小数位。然后,每次迭代从当前余数中获得商位,并为下一次迭代计算新的余数。那么,下式是迭代次数 \(it\) 计算公式:

\[ it = [n/log_2(4)] \]

其中 \(n\) 是结果的位数,包括舍入所需的位。除法的延迟,即周期数,与迭代次数直接相关。它还取决于每个周期执行的迭代次数。每个周期已经实现三次迭代以获得每个周期 \(6\) 位,这等效于 \(Radix-64\) 除法。那么规格化浮点数所需周期 \(cycles\) 由下式确定,除了迭代所需的(\(it/3\))个周期之外,有两个额外的周期用于操作数预缩放和舍入。

\[ cycles = [it/3] + 2 \]

数字迭代除法的一些实例,包括 \(Radix-4\) 算法,可以在[\(38\)]中找到。简单的实现如图所示。注意,只有余数的最高有效位用于选择商数位。余数使用进位保存加法器(\(CSA\))更新并存储在冗余表示中。然后,商数位选择需要余数的 \(t\) 个最高有效位在进位传播加法器(\(CPA\))中相加以获得其非冗余表示。但是这种实现太慢了,为了加速迭代的循环,需要使用迭代之间的余数计算和商数位选择中的推测算法。

三次radix-4组成radix-64的简单实现

操作数预缩放

在预缩放期间,除数被缩放到接近 \(1\) 的值,使得商数位的选择与除数无关。对于 \(radix-4\) 算法,缩放除数在 \([1 − 1/64,1+1/8]\) 范围内就足够了。如表预缩放因子真值表所示,仅三比特确定比例因子。注意预缩放时,除数应该被扩大 \(1-2\) 倍。被除数也应该缩放相同的比例因子。

预缩放因子真值表
\(0.1\)xxx 预缩放因子
\(000\) \(1+1/2+1/2\)
\(001\) \(1+1/4+1/2\)
\(010\) \(1+1/2+1/8\)
\(011\) \(1+1/2+0\)
\(100\) \(1+1/4+1/8\)
\(101\) \(1+1/4+0\)
\(110\) \(1+0+1/8\)
\(111\) \(1+0+1/8\)
整数位商计算

整数位商计算的同时,为数字迭代步骤(每个数字迭代执行三个 \(radix-4\),对应 \(s0,s1,s2\) 三个阶段)提供以下数据:

(1)冗余余数的进位保存表示:\(f\_r\_s,f\_r\_c\)

(2)预缩放后的除数:\(divisor\)

(3)为第一次数字迭代中 \(s0\) 阶段商选择提供 \(6\) 比特余数结果。

(4)为第一次数字迭代中 \(s1\) 阶段商选择提供 \(7\) 比特余数结果。

数字迭代

浮点除法器的实际实现需要每个周期执行三次 \(radix-4\) 迭代,常规顺序迭代三次速度太慢,无法满足时序要求,对逻辑进行了优化。图示出了数字迭代循环的框图。

数字迭代优化算法

(1)对除数进行处理,得到五种可能的商选择结果需要用到的除数倍数(商为负数时只取反)。

(2)\(s0\) 阶段,使用四个 \(CSA\) 模块(商为 \(0\) 时不需要),并行在 \(s0\) 预测性的计算出 \(s1\) 阶段需要的 \(5\) 个余数冗余表示。

(3)\(s0\) 阶段,使用第二步中计算出的 \(5\) 个余数冗余表示,为 \(s2\) 阶段预测性的计算 \(5\)\(7\) 比特余数结果。

(4)\(s0\) 阶段,根据输入信号中 \(6\) 比特余数结果,选择出 \(s0\) 阶段的商,商使用 \(5\) 比特独热码表示。

(5)根据 \(s0\) 阶段的商,选择出 \(s1\) 阶段需要使用的冗余余数表示,同时从第三步为 \(s2\) 阶段预测性的计算 \(5\)\(7\) 比特余数结果中选择出一个。

(6)\(s1\) 阶段,使用四个 \(CSA\) 模块(商为 \(0\) 时不需要),并行在 \(s1\) 预测性的计算出 \(s2\) 阶段需要的 \(5\) 个余数冗余表示。

(7)\(s1\) 阶段,根据输入信号中 \(7\) 比特余数结果、\(5\) 种商选择结果用到的的除数倍数、\(s0\) 阶段的商,预测性的进行 \(s1\) 阶段的商选择。

(8)根据 \(s1\) 阶段的商,选择出 \(s2\) 阶段需要使用的冗余余数表示。

(9)\(s2\) 阶段,使用四个 \(CSA\) 模块(商为 \(0\) 时不需要),并行在 \(s2\) 预测性的计算出下一个数字迭代 \(s0\) 阶段需要的 \(5\) 个余数冗余表示。

(10)\(s2\) 阶段,分别为下一次数字迭代中 \(s0\) 阶段需要的 \(6\) 比特余数结果和 \(s1\) 阶段需要的 \(7\) 比特余数结果预测性的计算 \(5\) 种可能的结果。

(11)\(s2\) 阶段,根据第五步中为 \(s2\) 阶段选出的的 \(7\) 比特余数结果、\(5\) 种商选择结果用到的的除数倍数、\(s1\) 阶段的商,预测性的进行 \(s2\) 阶段的商选择。

(12)根据 \(s2\) 阶段的商选择结果,为下一次数字迭代选择出:冗余余数的进位保存表示,\(s0\) 阶段需要的 \(6\) 比特余数结果,\(s1\) 阶段需要的 \(7\) 比特余数结果。

由于第一步中对除数的倍数只取反,没有 \(+1\),导致余数计算时会有偏差,在商选择过程中添加修正逻辑来修正,下表是标准的商选择函数,下表是逻辑修正后的商选择函数。

标准商选择函数
\(4 × rem[i]\) \(q_{i+1}\)
\([13/8,31/8]\) \(+2\)
\([4/8,12/8]\) \(+1\)
\([-3/8,3/8]\) \(0\)
\([-12/8,-4/8]\) \(-1\)
\([-32/8,-13/8]\) \(-2\)
逻辑修正后的商选择函数
\(4 × rem[i]\) \(carry\) \(q_{i+1}\)
\(31/8\) \(1\) \(+2\)
\([13/8,30/8]\) - \(+2\)
\(12/8\) \(0\) \(+2\)
\(12/8\) \(1\) \(+1\)
\([4/8,11/8]\) - \(+1\)
\(3/8\) \(0\) \(+1\)
\(3/8\) \(1\) \(0\)
\([-3/8,2/8]\) - \(0\)
\(-4/8\) \(0\) \(0\)
\(-4/8\) \(1\) \(-1\)
\([-12/8,-5/8]\) - \(-1\)
\(-13/8\) \(0\) \(-1\)
\(-13/8\) \(1\) \(-2\)
\([-32/8,14/8]\) - \(-2\)

将数字迭代输出的冗余余数表示恢复成标准余数,通过 \(On\) \(the\) \(Fly\) \(Conversion\) 将商和商减一计算出来,为商和商减一计算两套 \(grs\) 和是否需要向上舍入一位,计算选商或商减一的选择信号并选出正确的商结果,最后用正确的商结果和对应的是否需要向上舍入一位信号进行舍入。

非规格化数和提前终止

(1)输入含有非规格化数,非规格化数的有效数字小于 \(1\),无法和规格化数一起进行预缩放,为此多增加一个周期将非规格化数的有效数字进行规格化,并同时调整非规格化数的指数。

(2)结果是非规格化数,数字迭代后的商结果是大于 \(1\) 的,不符合非规格化有效数字范围,需要多增加一个周期对商结果右移来规格化。

(3)提前终止,有两种情况:当结果是 \(NaN\)、无穷、精确 \(0\) 的时候,提前终止计算输出结果,因为在第一周期就能得到这个信息,第二周期就可以输出除法结果;当除数是 \(2\) 的次幂时,其有效数字 \(=1\),除法只需对被除数的指数处理,可跳过数字迭代阶段,最快也可第二周期输出除法结果,但是当被除数或结果是非规格化数时,同样也需要额外的周期去处理。

向量浮点除法算法

对于向量浮点除法,\(RISC-V\) 向量指令集拓展没有混合精度的浮点除法,以此只需支持:

(1)\(1\)\(f64 = f64 + f64\);

(2)\(2\)\(f32 = f32 + f32\);

(3)\(4\)\(f16 = f16 + f16\)

考虑到向量除法计算中同时有多组除法计算,而提前终止功能会导致多组数据结果输出不同步,除非都是相同情况的提前终止,所以对于向量除法取消提前终止的机制,如果发生了提前终止的情况,让结果在内部暂存,和其他除法结果同时输出。

为了统一时序,将除法器的周期数统一规定为最差情况下的周期数,即输入含有非规格化数,输出也含有非规格化数的情况,其他原本可以更快输出结果的情况都进行内部数据暂存,等到规定的周期数后输出结果。

主体采用资源复用的方式进行设计,在非数字迭代模块进行如下数据复用:

(1)\(1\)\(f64/f32/f16 = f64/f32/f16 + f64/f32/f16\);

(2)\(1\)\(f32/f16 = f32/f16 + f32/f16\);

(3)\(2\)\(f16 = f16 + f16\)

共使用 \(4\) 组信号完成 \(7\) 组除法的功能。

由于数字迭代模块是关键路径,时序压力比较大,不能实现向非数字迭代模块部分的高度复用,否则时序不满足要求,为此对数字迭代模块进行部分复用设计:

(1)接口是 \(4\) 组商和冗余余数。

(2)\(s0\) 阶段采用 \(7\)\(CSA\)\(7\) 组预测,\(4\) 组商选择。

(3)\(s1,s2\) 阶段使用 \(4\)\(CSA\)\(4\) 组预测,\(4\) 组商选择。

对于寄存器也采用资源复用的方式,对于除数,冗余余数,商等寄存器,按照 \(max\)\(4\)\(f16\)\(2\)\(f32\)\(1\)\(f64\))所需的比特数分配大小。

硬件设计

向量浮点加法器

标量单一精度浮点加法器

根据改进的双通路浮点加法算法设计标量单一精度浮点加法器,其硬件实现架构如图所示。

标量单一精度浮点加法器架构图

左侧两个输入操作数 \(fp\_a\)\(fp\_b\),右侧 \(fp\_c\) 表示加法结果,\(fflags\)\(5\) 比特异常标志位,\(rm\) 是舍入模式,共五种舍入模式,使用 \(3\) 比特来表示,\(is\_sub\)\(0\) 时计算 \(fp\_c = fp\_a + fp\_b\)\(is\_sub\)\(1\) 时计算 \(fp\_c = fp\_a - fp\_b\),浮点加法与浮点减法的区别只在于 \(fp\_b\) 的符号位,所以只需要对 \(fp\_b\) 的符号位稍作处理就可以将浮点加法器同时支持浮点减法的计算。整体由三部分组成:\(far\) 通路、\(close\) 通路、异常通路。

\(far\) 通路先并行做两个规格化指数减法有效数字右移,分别处理 \(Efp\_a ≥ Efp\_b\)\(Efp\_b ≥ Efp\_a\) 的情况,根据 \(Efp\_a\)\(Efp\_b\) 大小关系选出正确的右移结果,送入 \(FS0\)\(FS1\) 两个有效数字加法器。\(far\) 通路对于减法时 \(EA\) 是大的指数减一,加法时 \(EA\) 是大的指数,这样有效数字加法结果的值域就在 \([1,4)\) 之间,右移时计算两套 \(grs\)\(grs\_normal\) 用于值域在 \([1,2)\) 之间的舍入,\(grs\_overflow\) 用于值域在 \([2,4)\) 之间的舍入。最后根据 \(FS0\) 的结果和舍入模式来选择 \(FS0\)\(FS1\) 作为尾数结果,同时根据舍入情况选择 \(EA\)\(EA+1\) 作为指数结果,符号位结果则根据指数大小情况就能得到,标志结果根据 \(EA+1\) 是否为全 \(1\) 判断是否上溢,根据 \(grs\) 判断是否不精确,\(far\) 通路不会产生除零,无效操作,下溢标志位。

\(close\) 通路使用 \(CS0、CS1、CS2、CS3\) 四个有效数字加法器处理 \(Efp\_a = Efp\_b\)\(Efp\_a = Efp\_b + 1\), \(Efp\_a = Efp\_b – 1\) 三种情况下的有效数字减法,然后根据 \(CS0\) 结果和 \(grs\) 产生四个独热选择信号 \(sel\_CS0\)\(sel\_CS1\)\(sel\_CS2\)\(sel\_CS3\),使用一个四输入独热码多路选择器 \(Mux1H\),选出一个结果和左移 \(mask\) 进行或操作,然后使用的优先级左移器进行尾数规格化,左移的同时输出 \(lzd\) 的值,指数结果为 \(EA – lzd\),尾数结果则需要在尾数规格化后和 \(CS4\) 之间选择一个作为尾数结果,\(CS4\) 是补充的舍入结果,不需要进行左移规格化。符号结果则由指数差和 \(CS0\) 的结果计算得到,标志结果只会产生不精确,其他异常标志位均不会产生。

异常通路是用来判断是否为无效操作,结果是否为 \(NaN\),结果是否为无穷,当这三种情况均为否时,进行正常计算,产生选择信号,选择 \(far\) 通路或 \(close\) 通路中的结果和标志位作为输出。

标量混合精度浮点加法器

在标量单一精度浮点加法器基础上进行混合精度硬件设计,主要区别是支持混合精度的计算,以结果为 \(f32\) 为例,下表是 \(res\_widen\)\(opb\_widen\) 对应操作的真值表。

结果为 \(f32\) 混合精度格式表
\(res\_widen\) \(opb\_widen\) \(f32\)
\(0\) \(0\) \(f32 = f32 + f32\)
\(1\) \(0\) \(f32 = f16 + f16\)
\(1\) \(1\) \(f32 = f16 + f32\)
\(0\) \(1\) 不允许

下图是标量混合精度浮点加法器架构图,其中最主要的区别是在数据输入端加一个快速格式转换模块,根据操作类型将操作数统一转换成结果的数据格式后,和单一精度浮点加法计算流程相同。

标量混合精度浮点加法器架构图

向量浮点加法器

下图是向量浮点加法器架构图,为了满足时序要求,采用四个模块组合而成,\(FloatAdderF64Widen\) 计算所有输出结果为 \(64\) 位的操作,\(FloatAdderF32WidenF16\) 计算所有输出结果为 \(16\) 位或 \(32\) 位的操作,\(FloatAdderF16\) 只计算输出结果为 \(16\) 位的操作。

其中 \(fp\_format\)\(2\) 比特结果格式控制信号,\(00\) 表示结果格式是 \(f16\)\(01\) 表示结果格式是 \(f32\)\(10\) 表示结果格式是 \(f64\),输出的标志位为 \(20\) 比特,低有效排列,当结果格式是 \(f16\) 时,\(20\) 比特全部有效,当结果格式是 \(f32\) 时,低 \(10\) 比特有效,当结果格式是 \(f64\) 时,低 \(5\) 比特有效。

向量浮点加法器架构图

向量浮点加法器采用两周期的流水线设计,为了实现快速唤醒,使加法结果 \(1.5\) 周期左右计算好。流水线划分在每个子模块内部进行,只需插入一级寄存器,下面介绍图中三种模块的流水线划分。

下图是 \(FloatAdderF64Widen\) 模块的流水线划分,\(far\) 通路在有效数字右移之后插入寄存器,\(close\) 通路在 \(Mux1H\) 后插入寄存器。

FloatAdderF64Widen 流水线划分

下图是 \(FloatAdderF32WidenF16\) 模块的流水线划分,该模块包括两种不同输出格式的计算,第二周期的选择逻辑比较复杂,所以在 \(far\) 通路种寄存器插在加法器之中,第一周期进行低 \(18\) 位的加法和高位部分的加法,第二周期将第一周期的低 \(18\) 位加法的进位和高位部分相加得到最终结果,\(close\) 通路也是在 \(Mux1H\) 后插入寄存器。

FloatAdderF32WidenF16 流水线划分

下图是 \(FloatAdderF16\) 模块的流水线划分,该模块时序压力最小,采用 \(far\) 通路在有效数字右移之后插入寄存器,\(close\) 通路在 \(Mux1H\) 后插入寄存器的划分方式。

FloatAdderF16 流水线划分

接口说明

之前介绍的向量浮点加法器宽度是 \(64\) 位的,输入数据要求两个操作数都是向量的形式,但是 \(RVV\) 不仅规定了两个操作数都是向量形式(\(vector-vector\),缩写\(vv\)),还规定了一个操作数是向量另一个操作数是标量的形式(\(vector-scalar\)缩写\(vf\)),另外在 \(widening\) 指令下,源操作数的排列也不仅仅只有低有效的情况,当源寄存器宽度是目的寄存器宽度的一半时,数据来源可能是低半部分也可能是高半部分。

为了实现 \(RVV\) 中所有浮点指令的计算操作,并且可进行 \(VLEN\) 拓展,在向量浮点加法器的基础上增加其他简单指令的计算,把它作为一个向量浮点"\(ALU\)",称作 \(VFALU\)

因此需要对向量浮点加法器进行改造来适配 \(RVV\) 的特性。改造分为两部分,一部分是功能改造,一部分是接口改造。

下表是 \(VFALU\) 支持的操作码,共 \(16\) 种操作,(\(w\))表示含有 \(widen\) 操作。\(vfmerge、vfmove、vfclass\) 操作数形式比较特殊:\(vfmerge.vfm\) 源操作数有三个:一个向量寄存器,一个浮点寄存器,一个 \(mask\) 寄存器;\(vfmove.v.f\) 源操作数只有一个浮点寄存器;\(vfclass\) 源操作数只有一个向量寄存器。

\(VFALU\)操作码
\(op\_code\) 对应指令 操作数形式 含义
\(0\) \(vf(w)add\) \(vv,vf\) 加法
\(1\) \(vf(w)sub\) \(vv,vf\) 减法
\(2\) \(vfmin\) \(vv,vf\) 求最小值
\(3\) \(vfmax\) \(vv,vf\) 求最大值
\(4\) \(vfmerge\) \(vfm\) 数据合并
\(5\) \(vfmove\) \(v.f\) 数据移动
\(6\) \(vfsgnj\) \(vv,vf\) 符号注入
\(7\) \(vfsgnjn\) \(vv,vf\) 反符号注入
\(8\) \(vfsgnjx\) \(vv,vf\) 异或符号注入
\(9\) \(vmfeq\) \(vv,vf\) 是否相等
\(10\) \(vmfnq\) \(vv,vf\) 是否不等
\(11\) \(vmflt\) \(vv,vf\) 是否小于
\(12\) \(vmfle\) \(vv,vf\) 是否小于等于
\(13\) \(vmfgt\) \(vf\) 是否大于
\(14\) \(vmfge\) \(vf\) 是否大于等于
\(15\) \(vfclass\) \(v\) 分类

下表是 \(VFALU\) 接口定义,相比于向量浮点加法器,增加了 \(widen\_a\)\(widen\_b\) 两个混合精度数据来源,当源操作数和目的操作数格式相同时,数据来自 \(fp\_a、fp\_b\),否则来自 \(widen\_a、widen\_b\),并且 \(uop\_idx=0\) 时取自低半部分, \(uop\_idx=1\) 时取自高半部分。\(is\_frs1=1\) 时源操作数 \(vs1\) 来自浮点寄存器 \(frs1\),需要复制成向量寄存器后参与计算, \(mask\) 参与 \(merge\) 指令的计算,\(op\_code\) 是操作码,表示进行的操作。

\(VFALU\) 接口和含义
接口 方向 位宽 含义
\(fp\_a\) \(input\) \(64\) 源操作数\(vs2\)
\(fp\_b\) \(input\) \(64\) 源操作数\(vs1\)
\(widen\_a\) \(input\) \(64\) \(widen\_vs2\)
\(widen\_b\) \(input\) \(64\) \(widen\_vs1\)
\(frs1\) \(input\) \(64\) 浮点寄存器数据
\(is\_frs1\) \(input\) \(64\) 加数来自浮点寄存器数据
\(mask\) \(input\) \(4\) 参与\(merge\) 指令计算
\(uop\_idx\) \(input\) \(1\) \(widen\) 时选择高/低半部分
\(round\_mode\) \(input\) \(3\) 舍入模式
\(fp\_format\) \(input\) \(2\) 浮点格式
\(res\_widening\) \(input\) \(1\) \(widen\) 指令
\(opb\_widening\) \(input\) \(1\) 源操作数\(vs1\)是否和结果格式相同
\(op\_code\) \(input\) \(5\) 操作码
\(fp\_result\) \(output\) \(64\) 计算结果
\(fflags\) \(output\) \(20\) 标志位

向量浮点融合乘加器

流水线划分

向量浮点融合乘加器采用四周期的流水线设计,为了实现快速唤醒,使乘加结果在 \(3.5\) 周期左右计算好。向量器的延迟为 \(3.5\) 周期,下图是向量浮点融合乘加器架构图,图中 \(reg\_0\) 表示第一级寄存器,\(reg\_1\) 表示第二级寄存器,\(reg\_2\) 表示第三级寄存器,向量浮点融合乘加器也支持 \(widen\) 功能,但是它的 \(widen\) 功能只有 \(f32 = f16 × f16 + f32\)\(f64 = f32 × f32 + f64\) 两种情况,所以输出格式确定的情况下,只需一比特 \(widen\) 信号控制即可。输出的 \(fflags\) 也是 \(20\) 比特的,和向量浮点加法器的表示方式一样。

向量浮点融合乘加器架构图

为了在满足时序约束的情况下节省面积,采用资源复用的实现方式,所有数据格式的计算通过同一个向量 \(Booth\) 编码器和 \(CSA\) 压缩,通过间隔摆放,\(107\) 比特加法器也实现了资源复用。

第一周期进行七组指数处理,得到七个右移值,根据计算格式选择对应的右移值,右移器方面将 \(f64\) 的右移器与一个 \(f32\) 共用,另外单独设置 \(1\)\(f32\)\(4\)\(f16\) 右移器,如果做减法 \(fp\_c\) 尾数右移结果要取反后输入到第一级寄存器。第一周期同时进行向量 \(Booth\) 编码,生成 \(27\) 个部分积,使用 \(CSA\) 压缩成 \(4\) 个部分积后寄存。

第二周期对剩余 \(4\) 个部分积进行 \(CSA4\_2\) 压缩后和第一周期计算的有效数字右移结果进行 \(CSA3\_2\) 压缩,然后进行 \(107\) 比特加法,寄存到第二级寄存器。

第三周期对第二周期的求和结果做 \(lzd\)\(tzd\) ,然后进行带 \(mask\) 限位的左移,左移结果存到第三级寄存器。

第四周期进行舍入得到尾数结果,根据第三周期左移情况计算出指数结果,符号位可由第二周期的 \(107\) 比特加法器得到,标志结果可以产生上溢、下溢、无效操作、不精确四种标志位。注意下溢的检测方式,\(IEEE-754\) 规定了两种检测下溢的方式,\(before \quad rounding\)\(after \quad rounding\),本设计使用 \(RISC-V\) 选取的 \(after \quad rounding\) 方式检测下溢。

接口说明

根据 \(RVV\) 的指令定义,可以使用向量浮点融合乘加器复用来计算乘法,由 \(op\_code\) 控制,当计算乘法时内部将加数置零,同时,\(RVV\)规定了一系列浮点融合乘加指令,主要是符号位和操作数顺序的区别。将向量浮点融合乘加器改造为支持所有相关指令的 \(VFMA\),增加 \(op\_code\) 和接口。下表是 \(VFMA\) 操作码,共 \(9\) 种操作,都支持 \(vv\)\(vf\) 两种操作数形式,当 \(vf\)\(vs1[i]\) 替换成浮点寄存器 \(frs1\)

\(VFMA\) 操作码
\(op\_code\) 对应指令 操作数形式 含义
\(0\) \(vf(w)mul\) \(vv,vf\) \(vd[i] = vs[2] × vs1[i]\)
\(1\) \(vf(w)macc\) \(vv,vf\) \(vd[i] = +(vs1[i] × vs2[i]) + vd[i]\)
\(2\) \(vf(w)nmacc\) \(vv,vf\) \(vd[i] = -(vs1[i] × vs2[i]) - vd[i]\)
\(3\) \(vf(w)msac\) \(vv,vf\) \(vd[i] = +(vs1[i] × vs2[i]) - vd[i]\)
\(4\) \(vf(w)nmsac\) \(vv,vf\) \(vd[i] = -(vs1[i] × vs2[i]) + vd[i]\)
\(5\) \(vfmadd\) \(vv,vf\) \(vd[i] = +(vs1[i] × vd[i]) + vs2[i]\)
\(6\) \(vfnamdd\) \(vv,vf\) \(vd[i] = -(vs1[i] × vd[i]) - vs2[i]\)
\(7\) \(vfmsub\) \(vv,vf\) \(vd[i] = +(vs1[i] × vd[i]) - vs2[i]\)
\(8\) \(vfnmsub\) \(vv,vf\) \(vd[i] = -(vs1[i] × vd[i]) + vs2[i]\)

下表是 \(VFMA\) 接口,为了简化控制逻辑的复杂度,送给 \(VFMA\) 三个操作数顺序固定为 \(vs2、vs1、vd\),由功能单元内部根据 \(op\_code\) 调换顺序,因为 \(fma\) 指令在 \(widen\) 时加数固定为目标格式,所以只需要增加 \(widen\_a\)\(widen\_b\)\(uop\_idx\) 同样用来控制选 \(widen\_a\)\(widen\_b\) 的高或低半部分,\(frs1\)\(is\_frs1\) 用来支持 \(vf\) 指令。

\(VFMA\)接口和含义
接口 方向 位宽 含义
\(fp\_a\) \(input\) \(64\) 源操作数\(vs2\)
\(fp\_b\) \(input\) \(64\) 源操作数\(vs1\)
\(fp\_c\) \(input\) \(64\) 源操作数\(vd\)
\(widen\_a\) \(input\) \(64\) \(widen\_vs2\)
\(widen\_b\) \(input\) \(64\) \(widen\_vs1\)
\(frs1\) \(input\) \(64\) 浮点寄存器数据
\(is\_frs1\) \(input\) \(64\) 加数来自浮点寄存器数据
\(uop\_idx\) \(input\) \(1\) \(widen\) 时选择高/低半部分
\(round\_mode\) \(input\) \(3\) 舍入模式
\(fp\_format\) \(input\) \(2\) 浮点格式
\(res\_widening\) \(input\) \(1\) \(widen\) 指令
\(op\_code\) \(input\) \(5\) 操作码
\(fp\_result\) \(output\) \(64\) 计算结果
\(fflags\) \(output\) \(20\) 标志位

向量浮点除法器

标量浮点除法器

标量浮点除法器支持三种格式的计算:\(1\)\(f16 = f16 / f16\)\(1\)\(f32 = f32 / f32\)\(1\)\(f64 = f64 / f64\)。除法器采用 \(Radix-64\) 算法,迭代模块每周期进行三次 \(Radix-4\) 迭代来实现 \(Radix-64\),下图是标量浮点除法器的架构图,除法器是阻塞执行的,计算过程中不能接受下一次除法操作,因此需要握手信号来控制,本设计采用\(start-vaild\)握手信号,由于 \(CPU\) 中会出现分支预测失败要 \(flush\) 掉流水线的状态,所以专门设置了 \(flush\) 信号用来清空除法器内部的状态,使之下周期立刻可以开始进行新的除法计算。

标量浮点除法器架构图

对于输入数据分为三种情况:都是规格化数(不含除数是 \(2\) 的次幂)、至少有一个是非规格化数、提前终止(输入含有 \(NaN\),无穷,零,或者除数是 \(2\) 的次幂)。结果分为两种情况:结果是规格化数、结果是非规格化数。

当输入都是规格化数(不含除数是 \(2\) 的次幂)时,尾数都是规格化的,此时直接进入预缩放阶段;当输入至少有一个是非规格化数时,相比于输入都是规格化数,在预缩放之前,要再加一个周期进行尾数规格化。

预缩放阶段为一个周期,接着进行整数商选择,将两比特整数商结果选择出来,并为 \(Radix-4\) 迭代提供预缩放后的除数被除数以及余数的保留进位冗余表示。\(Radix-4\) 迭代模块每周期计算 \(6\) 比特商,\(f16\) 除法需要 \(2\) 周期 \(Radix-4\) 迭代,\(f32\) 除法需要 \(6\) 周期 \(Radix-4\) 迭代,\(f64\) 除法需要 \(9\) 周期 \(Radix-4\) 迭代。\(Radix-4\) 迭代完成后,得到尾数商的结果的范围为\((1, 2)\)。当结果是规格化数时,只需要一个周期进行舍入和指数结果的计算,就可以得到最终的除法结果;当结果是非规格化数时,在舍入之前需要再加一个周期将商的结果进行非规格化。

提前终止分为两种情况:当输入操作数含有 \(NaN\),无穷,零的时候,不需要进行除法计算,第二个周期就可将结果输出;当除数是 \(2\) 的次幂时,第一个周期可求得指数结果,如果结果不需要非规格化步骤,第二周期可将结果输出;如果结果需要非规格化步骤,再加一个周期,第三个周期将结果输出。

下表是不同数据格式情况下标量除法器所需计算周期,\(+1\) 表示除法结果是非规格化数时再加一个周期进行后处理。提前终止情况下,只需 \(1\) ~ \(2\) 个周期完成所有数据格式除法操作,在非提前终止情况下,\(f16\) 除法需要 \(5\) ~ \(7\) 个周期完成除法,\(f32\) 除法需要 \(7\) ~ \(9\) 个周期完成除法,\(f64\) 除法需要 \(12\) ~ \(14\) 个周期完成除法.

标量除法器计算周期
数据格式 规格化数 非规格化数 提前终止
\(f16\) \(5+1\) \(6+1\) \(1+1\)
\(f32\) \(7+1\) \(8+1\) \(1+1\)
\(f64\) \(12+1\) \(13+1\) \(1+1\)

向量浮点除法器

下图是向量浮点除法器架构图,相比与标量浮点除法器,考虑到向量除法计算同时计算多组除法,所有结果计算完才能需要统一写回到寄存器堆,因此单个除法的提前终止对向量除法加速意义不大,所有取消了输出结果延迟不确定的特性,任何情况下,根据输入的数据格式,向量浮点除法器的延迟数是确定的,如下表所示。

向量浮点除法器架构图

向量除法器计算周期
数据格式 计算周期
\(f16\) \(7\)
\(f32\) \(11\)
\(f64\) \(14\)

在硬件设计上,除了 \(Radix-64\) 迭代模块,向量浮点除法器采用逻辑复用方式,使用四组信号进行计算和控制:第一组用于计算 \(f64\_0\)\(f32\_0\)\(f16\_0\),第二组用于计算 \(f32\_1\)\(f16\_1\),第三组用于计算 \(f16\_2\),第四组用于计算 \(f16\_3\)。寄存器方面也采用复用的方式来存储中间结果,寄存器位宽按照 \(max\)\(1\)\(f64\)\(2\)\(f32\)\(4\)\(f16\)),满足最大要求。而 \(Radix-64\) 迭代模块是关键路径,也在满足时序要求下尽量节省面积,第一次 \(Radix-4\) 迭代使用 \(7\) 组独立的 \(CSA\) 和商选择,第二、三次 \(Radix-4\) 迭代使用 \(4\) 组复用的 \(CSA\) 和商选择。

接口说明

\(RVV\) 规定的向量浮点除法指令有三个:

\(vfdiv.vv \quad vd[i] = vs2[i]/vs1[i]\)

\(vfdiv.vf \quad vd[i] = vs2[i]/f[rs1]\)

\(vfrdiv.vf \quad vd[i] = f[rs1]/vs2[i]\)

其中③比较特殊,它操作数的顺序和①②不同,对于向量除法单元,第一个操作数由控制逻辑传 \(vs2[i]/f[rs1]\),第二个操作数传 \(vs1[i]/ f[rs1]/vs2[i]\),所以功能单元看到的被除数是向量或标量形式,除数也是向量或标量形式,因此需要增加两个标量数据接口,增加接口后模块命名为 \(VFDIV\),接口如下表所示。

\(VFDIV\) 接口和含义
接口 方向 位宽 含义
\(start\_valid\_i\) \(input\) \(1\) 握手信号
\(finish\_ready\_i\) \(input\) \(1\) 握手信号
\(flush\_i\) \(input\) \(1\) 冲刷信号
\(fp\_format\_i\) \(input\) \(2\) 浮点格式
\(opa\_i\) \(input\) \(64\) 被除数
\(opb\_i\) \(input\) \(64\) 除数
\(frs2\_i\) \(input\) \(64\) 被除数来自浮点寄存器数据
\(frs1\_i\) \(input\) \(64\) 除数来自浮点寄存器数据
\(is\_frs2\_i\) \(input\) \(1\) 被除数来自浮点寄存器
\(is\_frs1\_i\) \(input\) \(1\) 除数来自浮点寄存器
\(rm\_i\) \(input\) \(3\) 舍入模式
\(start\_ready\_o\) \(output\) \(1\) 握手信号
\(finish\_valid\_o\) \(output\) \(1\) 握手信号
\(fpdiv\_res\_o\) \(output\) \(64\) 计算结果
\(fflags\_o\) \(output\) \(20\) 标志位

向量格式转换模块 VCVT

\(VCVT\) 模块是一个三级流水的向量浮点的格式转换模块。其例化 \(2\) 个具有 \(64bits\) 数据处理的子模块 \(VectorCvt\),每个 \(VectorCvt\) 分别包含一个 \(cvt64\),一个 \(cvt32\),两个 \(cvt16\) 模块。其中 \(cvt64\) 支持 \(64,32,16,8bits\) 浮点/整数格式的处理,\(cvt32\) 支持 \(32,16,8bits\) 浮点/整数格式的处理,\(cvt16\) 支持 \(16,8bits\) 浮点/整数格式的处理。即 \(vectorCvt\) 能同时处理 \(1\)\(64bits\)(或者 \(2\)\(32bits\),或者 \(4\) 个 16bits$,或者 \(4\)\(8bits\) )浮点/整数格式输入数据的转换。

总体设计

VCVT总体设计

模块设计

\(CVT\) 模块包含单宽度浮点/整型类型转换指令、加宽浮点/整型类型转换指令、缩小浮点/整型类型转换指令、向量浮点倒数平方根估计指令及向量浮点倒数估计指令。

根据 \(width\) 选择不同的 \(cvt\) 模块调用,\(cvt\) 模块的设计思路为根据指令类型分为 \(4\) 种,\(fp2int,int2fp,fp2fp,vfr\) 分别实现。\(fcvt64\) 的整体设计思路,把输入的 \(64bit\) 数据进行格式统一:

\(different \quad width \quad unsigned/signed \quad int -> 65 \quad signed \quad int\)

\(f16/f32/f64 -> 65bit(f64 \#\# false.B)\)

格式统一之后在转换过程中在一定程度上就不需要区分到底来的是哪种数据以及它们的位宽和字段的位置。

在此基础上,\(VFCVT64\) 分为 \(5\) 类:\(int -> fp,fp -> fp widen,fp -> fp narrow,estimate7(rsqrt7\) & \(rec7),fp -> int\)

FuopType 译码逻辑

对于 \(cvt\) 指令来说:其 \(fuopType\)\(9\) 位,每一位所表示的信息如下:

其中 \([5:0]\) 是从手册里得到的,\([8:6]\) 是为了方便生成控制信号设计时额外添加的。

\([8]:1\) 表示其是 \(move\) 指令,\(0\) 表示 \(cvt\) 指令或者 \(vfrsqrt7\)\(vfrec7\) 这两条估计指令。

\([7]:1\) 表示其输入是 \(fp\)\(0\) 表示输入是 \(int\)

\([6]:1\) 表示其输出是 \(fp\)\(0\) 表示输出是 \(int\)

\([5]:1\) 表示其是 \(vfrsqrt7\)\(vfrec7\) 这两条估计指令,否则是 \(cvt\) 指令;当其为 \(1\) 时,\([0]\) 用以区分其是 \(vfrsqrt7\) 还是 \(vfrec7\)

\([4:3]:00\) 表示 \(single\) 类型,\(01\) 表示 \(widen\)\(10\) 表示 \(narrow\)

\([2:0]\):对于不同的指令有不同的作用:对于浮点跟整数之间的转换,\([0]\) 用以区分整数是有符号数还是无符号数;其他情况下,\([2:1]=11\) 表示其是 \(rtz\) 类型指令,\([2:0]=101\) 表示是 \(rod(vfncvt_rod_ffw)\)