Describe the bug
When a python float is combined with a float64 array, the literal is first rounded to float32, and
only then promoted so float64_array * 0.1 computes with 0.10000000149011612 instead of 0.1. The
result's dtype is reported as float64, so nothing indicates that ~7 decimal digits were discarded.
MLX's weak-scalar promotion rule (a python scalar takes the array's category, not its
width, so it does not force a float32 array to float64). But applying it in the float64 direction
demotes the literal's value, which I think is not what is intended, there is no float32 array in the
expression to protect.
To Reproduce
import mlx.core as mx, numpy as np
with mx.stream(mx.cpu):
a = mx.array([1.0], dtype=mx.float64)
for lit in (0.1, 3.0, 1e-4, np.pi):
got = float(np.asarray(a * lit)[0]) # python float operand
wrapped = float(np.asarray(a * mx.array(lit, dtype=mx.float64))[0]) # explicit float64 operand
print(f"{lit!r:20s} dtype={(a*lit).dtype} got={got!r:22s} "
f"want={wrapped!r:22s} demoted={got != wrapped} float32(lit)={float(np.float32(lit))!r}")
0.1 dtype=float64 got=0.10000000149011612 want=0.1 demoted=True float32(lit)=0.10000000149011612
3.0 dtype=float64 got=3.0 want=3.0 demoted=False float32(lit)=3.0
0.0001 dtype=float64 got=9.999999747378752e-05 want=0.0001 demoted=True float32(lit)=9.999999747378752e-05
3.141592653589793 dtype=float64 got=3.1415927410125732 want=3.141592653589793 demoted=True float32(lit)=3.1415927410125732
Note got == float32(lit) exactly in every demoted case , the literal is going through float32.
Only float32-exact literals (3.0, 0.5, integers) are unaffected, which is what makes this so easy to
miss: half your constants look fine.
The same demotion reaches mx.pi, which is a python float:
import mlx.core as mx, numpy as np
with mx.stream(mx.cpu):
th = mx.array([20.0], dtype=mx.float64)
print(float(np.asarray(th * mx.pi / 180)[0])) # 0.34906586011...
print(float(np.asarray(th * mx.array(mx.pi, dtype=mx.float64) / 180)[0])) # 0.34906585039... (correct)
Expected behavior
Either (a) a python float in a float64 expression is used at full double precision, or (b) the operation
raises / warns, rather than silently reducing the value's precision while advertising float64.
Describe the bug
When a python
floatis combined with afloat64array, the literal is first rounded to float32, andonly then promoted so
float64_array * 0.1computes with0.10000000149011612instead of0.1. Theresult's dtype is reported as
float64, so nothing indicates that ~7 decimal digits were discarded.MLX's weak-scalar promotion rule (a python scalar takes the array's category, not its
width, so it does not force a float32 array to float64). But applying it in the float64 direction
demotes the literal's value, which I think is not what is intended, there is no float32 array in the
expression to protect.
To Reproduce
Note
got == float32(lit)exactly in every demoted case , the literal is going through float32.Only float32-exact literals (
3.0,0.5, integers) are unaffected, which is what makes this so easy tomiss: half your constants look fine.
The same demotion reaches
mx.pi, which is a python float:Expected behavior
Either (a) a python float in a float64 expression is used at full double precision, or (b) the operation
raises / warns, rather than silently reducing the value's precision while advertising
float64.