Skip to main content
欢迎来到PAWPAW技术文档网站了解更多信息

块浮点数(Block Floating-Point)-背景故事

32位固定点向量由int32_t尾数值的数组表示,同时伴随一个隐含的或(常量)显式的指数。

32位BFP(Block Floating-Point)向量由int32_t尾数和指数的数组表示,然而,指数必须是显式的,并且(通常)是动态的。此外,BFP向量还有另一个属性,称为它们的“头空间(headroom)”,它与尾数和指数一起被跟踪。

请注意,实际上并没有“块浮点”标量。这里的“块”意味着与单个指数相关联的尾数向量。只包含1个元素的BFP向量应被视为浮点值(尽管非标准)。

头空间(Headroom)

头空间是一个适用于整数标量和向量的概念。整数标量的头空间是“值在左移不丢失信息的位数”。

uint32_t23为例。

uint32_t x = 23;

作为一个无符号的32位整数,表示数字23的位模式为:

0000 0000 0000 0000 0000 0000 0001 0111

第一个非零位之前有27个0

那么,如果我们执行以下操作会发生什么(假设>>不是算术右移)?

printf("x: %u\n",  ((x<<27)>>27)  );

经过27位左移后,我们得到:

1011 1000 0000 0000 0000 0000 0000 0000

然后进行27位右移,我们回到了起始位置:

0000 0000 0000 0000 0000 0000 0001 0111

所以printf()输出:

x: 23

那么这个呢?

printf("x: %u\n",  ((x<<28)>>28)  );

经过28位左移后,我们得到:

0111 0000 0000 0000 0000 0000 0000 0000

1丢失了!现在进行右移后,我们得到:

0000 0000 0000 0000 0000 0000 0000 0111

所以printf()输出:

x: 7

左移28位导致信息丢失,而27位左移没有丢失信息。无符号32位整数x的头空间是27位,即它可以左移的位数而不丢失信息。

等价地,无符号整数的头空间(有时称为“无符号头空间”)可以定义为“前导零的位数”。我们左移掉的每个零在右移时都会被替换为零。


那么对于有符号整数呢?

我们同时考虑int32_t23-23的对:

int32_t x = 23;
int32_t y = -23;

它们的二进制补码表示为:

0000 0000 0000 0000 0000 0000 0001 0111 // x 1111 1111 1111 1111 1111 1111 1110 1001 // y

这次,让我们假设>>是一次算术右移:

printf("x: %d\n",  ((x<<27)>>27)  );
printf("y: %d\n", ((y<<27)>>27) );

输出结果是什么?

经过左移操作:

1011 1000 0000 0000 0000 0000 0000 0000 // x 0100 1000 0000 0000 0000 0000 0000 0000 // y

经过算术右移:

1111 1111 1111 1111 1111 1111 1111 0111 // x 0000 0000 0000 0000 0000 0000 0000 1001 // y

所以printf()输出:

x: -9 y: 9

27位左移破坏了我们的值!

那这个呢?

printf("x: %d\n",  ((x<<26)>>26)  );
printf("y: %d\n", ((y<<26)>>26) );

经过左移操作:

0101 1100 0000 0000 0000 0000 0000 0000 // x 1010 0100 0000 0000 0000 0000 0000 0000 // y

经过算术右移:

0000 0000 0000 0000 0000 0000 0001 0111 // x 1111 1111 1111 1111 1111 1111 1110 1001 // y

所以printf()输出:

x: 23 y: -23

这次左移27位导致信息丢失,而左移26位没有丢失。

对于有符号整数的头空间的等价定义(有时称为“有符号头空间”)是多余的前导符号位数。

对于23-23,最高位的27位都是符号位。但我们只需要(至少)1个符号位。所以其中26位是多余的符号位。因此,23-23的头空间是26位。

请记住,非负值的编码对于有符号和无符号值是相同的。一个经验法则是,对于非负值,有符号头空间无符号头空间少1位。


请注意,对于有符号整数x-x,它们的头空间不总是相同。特别地,如果x是2的整数幂,则-x的头空间将多1位。

我们来看一下int8_t32-32

0010 0000 // 32 1110 0000 // -32

我们可以看到32只有2位符号位,而-323位符号位,使得它们的头空间分别为1位和2位。

选择指数

通常,在执行输出BFP向量的BFP操作时,第一步是选择输出向量将使用的指数。这是在查看尾数值之前完成的 - 实际上,可以在没有访问尾数的情况下完成。为什么?如何实现?

为什么要在执行操作之前选择指数?因为我们必须确保不会溢出输出向量的元素。

考虑lib_xcore_math的Vector API中的vect_s16_mul()函数。该函数接受两个int16_t数组,并逐元素将它们相乘,产生一个int16_t输出向量。

C_API
headroom_t vect_s16_mul(
int16_t a[],
const int16_t b[],
const int16_t c[],
const unsigned length,
const right_shift_t a_shr);

该函数执行的操作是:

a[k](b[k]c[k])2a_shr对于 k=0,1,2,...(length1)\begin{aligned} &\mathtt{a}[k] \gets \frac{(\mathtt{b}[k] \cdot \mathtt{c}[k])} {2^{\mathtt{a\_shr}}} \\ &\qquad\text{对于 }k = 0,1,2,...(\mathtt{length}-1) \end{aligned}

与32位乘法不同,这种情况下的XS3 VPU不会自动对乘积应用固定的右移操作。因此,在a_shr0的特定情况下,该函数尝试将每个a[k]分配给b[k]c[k]的32位乘积。问题是,大多数16位数对的乘积都无法适应16位数!

一种解决方法是避免使用vect_s16_mul(),而是直接使用32位乘积。但是当您需要对32位向量进行逐元素乘积时会怎么样呢?您可以使用64位向量来存储结果。然后是128位向量?

由于明显的原因,这通常是不可行的。在某些情况下,这可能是一种有效的方法,但在其他情况下,相同的对象必须递归更新,每次内存翻倍。解决方案是接受一些量化误差并使用BFP向量,而vect_s32_mul()就是为此而设计的。

为了使BFP工作,我们必须选择一个输出向量的指数,其中没有输出元素溢出。

一种方法是采用两步方法。我们可以在第一次遍历中计算每个输出元素,仅确定允许存储每个值的最小指数。然后在第二次遍历中,我们可以使用正确的指数重新计算每个元素。

但这样做很浪费。


仍然考虑我们具有输入向量bˉ\bar{b}cˉ\bar{c}以及输出向量aˉ\bar{a}的16位情况,为了确定a^\hat{a},与aˉ\bar{a}相关联的指数,我们应该找出输出元素的可能逻辑值的范围。

bkck=b[k]2b^c[k]2c^=2b^+c^b[k]c[k]INT16_MINb[k]INT16_MAXINT16_MINc[k]INT16_MAXINT16_MIN=215INT16_MAX=2151215(2151)b[k]c[k](215)2230+215b[k]c[k]230230<b[k]c[k]2302302b^+c^<2b^+c^b[k]c[k]2302b^+c^230+b^+c^<2b^+c^b[k]c[k]230+b^+c^\begin{aligned} & b_k \cdot c_k \\ &= \mathtt{b}[k] \cdot 2^{\hat{b}} \cdot \mathtt{c}[k] \cdot 2^{\hat{c}} \\ &= 2^{\hat{b}+\hat{c}} \cdot \mathtt{b}[k] \cdot \mathtt{c}[k] \\ \\ \mathtt{INT16\_MIN} \le &\mathtt{b}[k] \le \mathtt{INT16\_MAX} \\ \mathtt{INT16\_MIN} \le &\mathtt{c}[k] \le \mathtt{INT16\_MAX} \\ \mathtt{INT16\_MIN} &= -2^{15} \\ \mathtt{INT16\_MAX} &= 2^{15}-1 \\ \\ -2^{15}\cdot (2^{15}-1) \le \,&\mathtt{b}[k] \cdot \mathtt{c}[k] \le (-2^{15})^2 \\ -2^{30} + 2^{15} \le \,&\mathtt{b}[k] \cdot \mathtt{c}[k] \le 2^{30} \\ -2^{30} < \,&\mathtt{b}[k] \cdot \mathtt{c}[k] \le 2^{30} \\ -2^{30} \cdot 2^{\hat{b}+\hat{c}} < \,2^{\hat{b}+\hat{c}} \cdot &\mathtt{b}[k] \cdot \mathtt{c}[k] \le 2^{30} \cdot 2^{\hat{b}+\hat{c}} \\ -2^{30+\hat{b}+\hat{c}} < \,2^{\hat{b}+\hat{c}} \cdot &\mathtt{b}[k] \cdot \mathtt{c}[k] \le 2^{30+\hat{b}+\hat{c}} \\ \end{aligned}

每个aka_k都在范围(230+b^+c^,230+b^+c^](-2^{30+\hat{b}+\hat{c}}, 2^{30+\hat{b}+\hat{c}}]内。由于上界有等号,我们可以只考虑这种情况。

我们可以通过以下方式找到将230+b^+c^2^{30+\hat{b}+\hat{c}}存储在形式a[k]2a^\mathtt{a}[k] \cdot 2^{\hat{a}}中所需的最小a^\hat{a}

a[k]\mathtt{a}[k]是一个有符号的16位整数,它可以存储小于2152^{15}的正值。如果我们假设a[k]\mathtt{a}[k]2152^{15},那么

a[k]2a^=230+b^+c^2152a^=230+b^+c^215+a^=230+b^+c^15+a^=30+b^+c^a^=15+b^+c^\begin{aligned} \mathtt{a}[k] \cdot 2^{\hat{a}} &= 2^{30+\hat{b}+\hat{c}} \\ 2^{15}\cdot 2^{\hat{a}} &= 2^{30+\hat{b}+\hat{c}} \\ 2^{15+\hat{a}} &= 2^{30+\hat{b}+\hat{c}} \\ 15 + \hat{a} &= 30 + \hat{b} + \hat{c} \\ \hat{a} &= 15 + \hat{b} + \hat{c} \end{aligned}

然而,a[k]\mathtt{a}[k]不能存储一个值为2152^{15}的数,它最多可以存储(2151)(2^{15}-1)。所以,在这里我们需要做一个选择。我们可以使用a^=15+b^+c^\hat{a} = 15+\hat{b}+\hat{c},并将任何a[k]\mathtt{a}[k]的值饱和为2152^{15}时都饱和为(2151)(2^{15}-1)。或者我们可以使用a^=14+b^+c^\hat{a} = 14+\hat{b}+\hat{c},这样就不需要饱和,因为这是不可能的。

这两种选择都涉及到1个最低有效位的误差,并且可能在不同的应用中涉及到不同的权衡。

幸运的是,因为我们已经解决了通用的16位情况,所以我们不需要每次进行完整的推导,我们只需要使用例如a^=14+b^+c^\hat{a} = 14+\hat{b}+\hat{c}

一旦我们有了a^\hat{a},我们可以使用它来找到应该应用于累加器的右移r\vec{r},通过vect_s16_mul()acc_shr参数。

从我们拥有的指数(b^+c^\hat{b}+\hat{c},16位乘法的结果)到所需的指数a^\hat{a}的右移r\vec{r}的转换是一个简单的减法:

ra^(b^+c^)r(14+b^+c^)(b^+c^)r14\begin{aligned} \vec{r} &\gets \hat{a} - (\hat{b} + \hat{c}) \\ \vec{r} &\gets (14+\hat{b}+\hat{c}) - (\hat{b} + \hat{c}) \\ \vec{r} &\gets 14 \end{aligned}

所以,我们看到将16位BFP向量bˉ\bar{b}cˉ\bar{c}相乘会导致指数增加14。如果我们需要将aˉ\bar{a}与另一个16位BFP向量相乘,指数是否会再增加14?指数是否会随着连续乘法而不断增加,不考虑被乘数的头空间,直到所有元素四舍五入为零?

阻止这种情况发生的一种方法是从生成的BFP向量中去除头空间。我们使用最大可能的输出来确定指数。在大多数情况下,我们不会得到最大的可能值,因此我们的生成的BFP向量可能会有头空间。

计算出a^\hat{a}后,可以计算其头空间hah_a,然后我们可以通过移位操作将每个a[k]\mathtt{a}[k]左移hah_a位,并将hah_a加到指数a^\hat{a}中,从而“规范化”BFP向量。

您可能已经注意到,lib_xcore_math中表示BFP向量的结构体具有用于跟踪向量头空间的字段headroom_t hr

那么,如果我们在计算aˉ\bar{a}之后保留头空间呢?我们只需跟踪它。在lib_xcore_math中,表示BFP向量的结构体有一个名为headroom_t hr的字段,用于跟踪向量的头空间。

那么,如果我们跟踪bˉ\bar{b}cˉ\bar{c}的头空间,并且我们知道我们的输入尾数向量b[]\mathtt{b}[\,]c[]\mathtt{c}[\,]的头空间分别为hbh_bhch_c,我们如何结合这些信息?

回想一下,头空间的一种解释是数据在左移时不丢失信息的位数。这意味着数据理论上可以存储在比特深度更小的整数中,这又意味着可能的值范围更小。

具体来说,如果具有0位头空间的int16_t的范围是从INT16_MININT16_MAX,那么具有n位头空间的int16_t值的可能值范围将是从(INT16_MIN>>n)(INT16_MAX>>n)

如果我们具有指数b^\hat{b}c^\hat{c}以及头空间hbh_bhch_cbˉ\bar{b}cˉ\bar{c},那么我们可以应用类似上面的逻辑来发现

230(hb+hc)<b[k]c[k]230(hb+hc)-2^{30-(h_b+h_c)} < \mathtt{b}[k] \cdot \mathtt{c}[k] \le 2^{30-(h_b+h_c)}

这然后告诉我们可以找到输出指数为

a^=14+(b^+c^)(hb+hc)\hat{a} = 14 + (\hat{b} + \hat{c}) - (h_b + h_c)

使用这种选择输出指数的新方法,我们避免了指数增长并持续增长的情况,而不考虑尾数向量的头空间。