Python numpy float16 datatype operaciones, y float8?

al realizar operaciones matemáticas en números float16 Numpy, el resultado también está en el número de tipo float16. Mi pregunta es ¿cómo se calcula exactamente el resultado? Diga que estoy multiplicando / sumndo dos números float16. ¿Python genera el resultado en float32 y luego trunca / redondea el resultado a float16? ¿O es el cálculo realizado en ‘hardware multiplexor / sumdor de 16 bits’ hasta el final?

otra pregunta – ¿hay un tipo float8? No pude encontrar este … si no, entonces ¿por qué? ¡Gracias a todos!

A la primera pregunta: no hay soporte de hardware para float16 en un procesador típico (al menos fuera de la GPU). NumPy hace exactamente lo que sugiere: convierta los operandos de float32 a float32 , realice la operación escalar en los valores de float32 , luego redondee el resultado de float32 nuevo a float16 . Se puede demostrar que los resultados todavía están correctamente redondeados: la precisión de float32 es lo suficientemente grande (en relación con la de float16 ) que el redondeo doble no es un problema aquí, al menos para las cuatro operaciones aritméticas básicas y la raíz cuadrada.

En la fuente NumPy actual, esto es lo que parece la definición de las cuatro operaciones aritméticas básicas para las operaciones escalares float16 .

 #define half_ctype_add(a, b, outp) *(outp) = \ npy_float_to_half(npy_half_to_float(a) + npy_half_to_float(b)) #define half_ctype_subtract(a, b, outp) *(outp) = \ npy_float_to_half(npy_half_to_float(a) - npy_half_to_float(b)) #define half_ctype_multiply(a, b, outp) *(outp) = \ npy_float_to_half(npy_half_to_float(a) * npy_half_to_float(b)) #define half_ctype_divide(a, b, outp) *(outp) = \ npy_float_to_half(npy_half_to_float(a) / npy_half_to_float(b)) 

El código anterior se toma de scalarmath.c.src en la fuente NumPy. También puede echar un vistazo a loops.c.src para obtener el código correspondiente para los ufuncs de matriz. Las npy_half_to_float soporte de npy_half_to_float y npy_float_to_half se definen en halffloat.c , junto con otras funciones de soporte para el tipo float16 .

Para la segunda pregunta: no, no hay float8 tipo float8 en NumPy. float16 es un tipo estandarizado (descrito en el estándar IEEE 754), que ya se usa ampliamente en algunos contextos (en particular las GPU). No hay float8 tipo IEEE 754 float8 , y no parece haber un candidato obvio para un tipo “estándar” float8 . También supongo que simplemente no ha habido tanta demanda de soporte de float8 en NumPy.

Esta respuesta se basa en el aspecto float8 de la pregunta. La respuesta aceptada cubre el rest bastante bien. Una de las razones principales por la que no existe un tipo float8 ampliamente aceptado, aparte de la falta de estándar, es que no es muy útil en la práctica.

Primer en punto flotante

En la notación estándar, un tipo de datos float[n] se almacena utilizando n bits en la memoria. Eso significa que, como máximo, solo se pueden representar 2^n valores únicos. En IEEE 754, un puñado de estos valores posibles, como nan , no son números pares como tales. Eso significa que todas las representaciones de punto flotante (incluso si se va float256 ) tienen espacios en el conjunto de números racionales que pueden representar y se redondean al valor más cercano si intenta obtener una representación para un número en este espacio. En general, cuanto más alta es la n , más pequeñas son estas brechas.

Puede ver la brecha en acción si usa el paquete struct para obtener la representación binaria de algunos números float32 . Es un poco sorprendente encontrarlo al principio, pero hay un espacio de 32 solo en el espacio entero:

 import struct billion_as_float32 = struct.pack('f', 1000000000 + i) for i in range(32): billion_as_float32 == struct.pack('f', 1000000001 + i) // True 

En general, el punto flotante es mejor para rastrear solo los bits más significativos, de modo que si sus números tienen la misma escala, se conservan las diferencias importantes. Los estándares de punto flotante generalmente difieren solo en la forma en que distribuyen los bits disponibles entre una base y un exponente. Por ejemplo, IEEE 754 float32 usa 24 bits para la base y 8 bits para el exponente.

De vuelta a float8

Por la lógica anterior, un valor float8 solo puede tomar 256 valores distintos, sin importar lo inteligente que sea al dividir los bits entre la base y el exponente. A menos que esté interesado en redondear los números a uno de los 256 números arbitrarios agrupados cerca de cero, es probablemente más eficiente simplemente rastrear las 256 posibilidades en un int8 .

Por ejemplo, si desea realizar un seguimiento de un rango muy pequeño con una precisión aproximada, puede dividir el rango que desea en 256 puntos y luego almacenar a cuál de los 256 puntos se encontraba más cerca de su número. Si quisiera volverse realmente lujoso, podría tener una distribución no lineal de valores agrupados en el centro o en los bordes, según lo que más le importara.

La probabilidad de que alguien más (o incluso usted mismo más adelante) necesite este esquema exacto es extremadamente pequeña y, en la mayoría de los float16 el byte adicional o 3 que paga como penalización por usar float16 o float32 es demasiado pequeño para hacer una diferencia significativa. Por lo tanto … casi nadie se molesta en escribir una implementación float8 .