Estou tentando entender como o NumPy implementa o arredondamento para o mais próximo, mesmo ao converter para um formato de precisão mais baixa, neste caso, Float32 para Float16, especificamente o caso em que o número é normal em Float32, mas é arredondado para um subnormal em Float16.
Link para o código: https://github.com/numpy/numpy/blob/13a5c4e569269aa4da6784e2ba83107b53f73bc9/numpy/core/src/npymath/halffloat.c#L244-L365
Meu entendimento é o seguinte,
Em float32, o número tem os bits
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
e | e0 | e1 | e2 | e3 | e4 | e5 | e6 | e7 | m0 | m1 | m2 | m3 | m4 | m5 | m6 | m7 | m8 | m9 | m10 | m11 | m12 | m13 | m14 | m15 | m16 | m17 | m18 | m19 | m20 | m21 | m22 |
/*
* If the last bit in the half significand is 0 (already even), and
* the remaining bit pattern is 1000...0, then we do not add one
* to the bit after the half significand. However, the (113 - f_exp)
* shift can lose up to 11 bits, so the || checks them in the original.
* In all other cases, we can just add one.
*/
if (((f_sig&0x00003fffu) != 0x00001000u) || (f&0x000007ffu)) {m
f_sig += 0x00001000u;
}
O código acima é usado ao quebrar empates para o par mais próximo. Não entendo por que na segunda parte do OR lógico, fazemos AND bit a bit contra 0x0000'07ffu
(bits m12-m22) e não 0x0000'ffffu
(m11-m22) .
Depois de alinharmos os bits da mantissa para que fiquem no formato subnormal para float16 (que é o que a mudança de bits antes deste pedaço de código faz), na representação numérica float32 acima teríamos m10
- m22
decidindo qual direção arredondar.
Meu entendimento é que a segunda parte do OR verifica se o número é maior do que o ponto médio, e se for, então adiciona um ao bit meio-significante. Mas com o número original, ele não está apenas verificando um subconjunto dos números que estão acima do ponto médio? No número float16, m9 seria a última precisão que vai permanecer. Então, arredondaremos para cima se,
m9 é 1, m10 é 1 e m11-m22 são todos 0 (A primeira parte do OR)
m10 é 1, pelo menos um de m11-m22 é 1 (para colocar o número acima do ponto médio)
pode ser simplificado adicionando 1 a m10, se qualquer um de m11-m22 for 1. se m10 já for 1, a adição sangrará para m9, caso contrário, permanecerá inalterado. Mas, no caso do código NumPy, os bits verificados são m12-m22.
Não tenho certeza do que estou perdendo aqui. Esse é um cenário de caso especial?
Eu esperava que os bits m11-m22 fossem os que decidiriam se adicionariam 1 e nem m12-m22.
f_sig
contém um significando-em-preparação para o resultado binary16. ( binary16 é o nome IEEE-754 para o que algumas pessoas chamam de formato de ponto flutuante de “meia precisão”.) Neste ponto, o código precisa dos bits de significando nos bits 22:13, porque mais tarde ele vai deslocá-los em mais 13 bits, colocando-os em 9:0. Em preparação para isso, ele deslocou os bits de acordo com o expoente. Isso deslocou alguns bits def_sig
.Agora ele quer testar se o bit baixo do novo significando (agora no bit 13) é 0, o mais alto dos bits abaixo do significando (no bit 12) é 1, e todos os bits restantes são 0. Alguns desses bits restantes estão nos bits 11:0 de
f_sig
. Mas alguns deles podem ter desaparecido. O deslocamento de acordo com o expoente deslocou alguns deles para fora. Então, para testar se esses bits são 0, olhamos para eles no significando original emf
.Como o deslocamento do expoente se deslocou para fora no máximo 11 bits, só precisamos olhar para os 11 bits baixos de
f
. Os outros bits do significando original ainda estão presentes emf_sig
.Então, em
(f_sig&0x00003fffu) != 0x00001000u) || (f&0x000007ffu)
, o operando esquerdo de||
testa os bits de significando originais que estãof_sig
e o operando direito testa os bits de significando originais que estão emf
. Pode haver alguma sobreposição; o último pode testar alguns bits que também estão emf_sig
, mas isso não importa.Não, não está verificando isso. O teste é verdadeiro se e somente se a porção final não for exatamente ½ do bit menos significativo (LSB) do novo significando ou o bit menos significativo for 1.
O raciocínio é este:
f_sig += 0x00001000u;
, adiciona ½ do LSB, e o significando é posteriormente truncado no LSB (f_sig >> 13
). Isso fornece o arredondamento desejado na maioria dos casos: Adicionar ½ às porções finais menores que ½ não carrega, e adicionar ½ às porções finais maiores que ½ carrega.