Skip to content

[BUG] A python-float operand silently rounds through float32 in float64 arithmetic (result dtype stays float64) #4159

Description

@kyrollosyanny

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions