Skip to content

random.randint loses integer range semantics through float32 #3926

Description

@jasp-nerd

Describe the bug

mx.random.randint is documented to sample integers with equal probability
from [low, high). When the bounds are not exactly representable in
float32, it can return the excluded high value, omit valid integers, or
collapse a valid int64 interval to a constant.

To Reproduce

import mlx.core as mx

key = mx.random.key(42)

for stream in (mx.cpu, mx.gpu):
    x = mx.random.randint(
        2**24, 2**24 + 2, (10_000,),
        dtype=mx.int32, key=key, stream=stream,
    )
    y = mx.random.randint(
        2**40, 2**40 + 1024, (10_000,),
        dtype=mx.int64, key=key, stream=stream,
    )
    mx.eval(x, y)
    print(stream, sorted(set(x.tolist())), len(set(y.tolist())))

Observed on both CPU and Metal:

cpu [16777216, 16777218] 1
gpu [16777216, 16777218] 1

In the first call, 16777218 is high and should never be returned, while the
valid value 16777217 is absent. The second call produces only 2**40.

Expected behavior

Every result should be in [low, high), and every integer in the interval
should be reachable with equal probability.

Root cause

The current implementation routes randint through a float32 uniform
sample:

auto u = uniform(low, high, shape, float32, key, s);
return astype(maximum(u, low, s), dtype, s);

At 2**24, adjacent float32 values are two integers apart. At 2**40, their
spacing is 2**17, so the integer interval is lost before the final cast. The
relevant implementation is unchanged on current main.

Desktop

  • macOS 26.5.2, arm64
  • MLX 0.32.0
  • Reproduced on mx.cpu and mx.gpu

Additional context

I searched open and closed issues and PRs for this behavior and did not find a
duplicate. #429 / #457 concern global seeding, #1932 is a docstring fix, and
#2361 concerns floating uniform sampling. The review discussion in #2361
seems relevant because it treats loss of reachable random values as a major
bug and calls for a long-term fix to recurring sampling issues.

A clamp to high - 1 would prevent the out-of-bounds result but would retain
missing values and severe bias. Before preparing a patch, I would like to
confirm whether an exact bounded-integer sampler based on the existing
Threefry words is the preferred direction. If so, I am happy to put up a
focused PR with boundary regression tests.

I put this report together with help from an AI coding assistant, mainly to
research previous issues and PRs and cross-check the relevant implementation.
I verified the reproduction and reviewed the report before posting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions