Skip to content

Gate MLX negative-stride materialization before v0.32.0 - #2259

Open
cetagostini wants to merge 9 commits into
pymc-devs:mainfrom
cetagostini:mlx-subtensor-negative-stride
Open

Gate MLX negative-stride materialization before v0.32.0#2259
cetagostini wants to merge 9 commits into
pymc-devs:mainfrom
cetagostini:mlx-subtensor-negative-stride

Conversation

@cetagostini

@cetagostini cetagostini commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

MLX v0.32.0 fixed compiled-kernel correctness for negative-strided inputs (#3720). This keeps the Subtensor mx.contiguous workaround only for negative-step slices on MLX versions before 0.32.0, so older MLX releases remain correct while fixed releases avoid an unnecessary materialization.

Reproducer and version evidence

import mlx.core as mx
import numpy as np

x = mx.array(np.arange(15, dtype="float32").reshape(5, 3))
out = mx.compile(lambda x: 2.0 * x[::-1])(x)
mx.eval(out)
MLX version Result
0.31.2 Incorrect: only the first reversed row is correct; the remaining rows are zero.
0.32.0 Correct: exactly matches the NumPy result.

The v0.32.0 release notes list “Fix compiled kernel correctness for negative-strided inputs” (#3720). PyTensor does not declare an MLX minimum and its MLX tests use unversioned pytest.importorskip; more concretely, the current MLX CI job installs mlx<0.29.4 (0.29.3), so it requires the compatibility branch. CI exercises the pre-0.32.0 materialization path; the recorded MLX 0.32.0 test run below exercises the fixed no-copy path.

The regressions run on every MLX version: first- and second-axis negative steps, a non-unit negative step, and simultaneous reversal of both axes, each feeding an elementwise op under mode="MLX"; the previously-xfailed IncSubtensor negative-step gradient path also passes.

Stacked dependency

This branch is stacked on #2258. Until #2258 merges, this PR's diff includes its Scan dispatch and reusable ScanCompatibilityTests coverage; please review #2258 first, then this focused Subtensor change. Ricardo's requested Scan edge cases are present and run with mode="MLX": higher-order derivatives and aliased inner outputs.

Test plan

  • Bare mx.compile(lambda x: 2.0 * x[::-1]): RED on MLX 0.31.2 and GREEN on 0.32.0.
  • Old-MLX bare checks for first axis, second axis, non-unit step, and both-axis reversal: all fail without materialization and pass with mx.contiguous on MLX 0.31.2.
  • PYTENSOR_FLAGS="base_compiledir=/Users/carlostrujillo/Documents/GitHub/_worktrees/pr-2259/.pytensor_compile" /opt/anaconda3/envs/pytensor-pr-2259/bin/pytest -q tests/link/mlx/test_subtensor.py tests/link/mlx/test_scan.py on MLX 0.32.0 — 41 passed, 2 pre-existing xfailed (boolean indexing and inplace operations).
  • /opt/anaconda3/envs/pytensor-pr-2259/bin/ruff check and ruff format --check on the touched files.

🤖 Generated with assistance from Cursor.

Made with Cursor

Port the JAX scan dispatch to MLX. As MLX has no native general-scan
primitive, the inner fgraph is driven by a Python carry loop that
`mx.compile` unrolls. Because scalar values are not readable while MLX
traces, the (full-sized) recurring buffers are used to infer the number
of steps, falling back to a constant `n_steps` or the sequence length.

Covers seqs, MIT-SOT, SIT-SOT, NIT-SOT, untraced SIT-SOT, MIT-MOT (scan
gradients) and non-sequences, mirroring the JAX semantics of recreating
the trace and prepending/truncating to the buffer size.

Gradients over sequences reverse the trace and currently trip a separate
MLX bug (an elementwise op fed by a negative-stride array is miscompiled
under `mx.compile`); this is captured as a strict xfail under the full
`mode="MLX"` and is addressed by a follow-up.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cetagostini
cetagostini marked this pull request as draft June 26, 2026 09:55
@cetagostini cetagostini self-assigned this Jun 26, 2026
cetagostini and others added 2 commits June 26, 2026 13:33
Clarify in the dispatch that the Python carry loop is unrolled into the
graph at trace time (not by `mx.compile`) and that this is a workaround
until MLX exposes a native scan/while primitive (ml-explore/mlx#1441):
it needs a static step count and the graph grows as O(n_steps * inner_ops),
where a real primitive would keep it O(inner_ops).

Co-authored-by: Cursor <cursoragent@cursor.com>
`mx.compile` miscompiles an elementwise op fed by a negative-stride view
(`mx.compile(lambda x: 2.0 * x[::-1])` zeroes the trailing entries; eager
is correct). The MLX Subtensor dispatch now copies reversed slices into a
contiguous array.

This unblocks Scan gradients over sequences (which reverse the trace) under
the full `mode="MLX"`, and resolves the existing strict xfail
`test_mlx_IncSubtensor_negative_step_slice_grad` — whose failure was the same
negative-stride read feeding the elementwise gradient term, not the
IncSubtensor write it was attributed to (ml-explore/mlx#3716).

Co-authored-by: Cursor <cursoragent@cursor.com>
@cetagostini
cetagostini force-pushed the mlx-subtensor-negative-stride branch from 273b91c to 85e3f6d Compare June 26, 2026 10:33

@ricardoV94 ricardoV94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some edge cases of Scan packaged as reusable tests that you can pass mode to, you should try them

@cetagostini

Copy link
Copy Markdown
Contributor Author

I'll redo this if mlx patch get merge: ml-explore/mlx#3772

cetagostini and others added 5 commits July 2, 2026 18:13
check_aliased_inner_outputs passes as-is (static and dynamic shapes).
check_higher_order_derivative is correct with use_compile=False but is
a strict xfail under mx.compile: the second-order backward Scan reads
the forward trace through negative-stride Subtensor views, hitting the
same MLX miscompilation already xfailed for first-order sequence grads.
The shared check gains an rtol knob since MLX casts float64 to float32.
check_grad_until_and_truncate_sequence_taps needs a while Scan, which
the MLX dispatch (like JAX) does not support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cetagostini cetagostini changed the title Materialize negative-stride MLX Subtensor results Gate MLX negative-stride materialization before v0.32.0 Jul 28, 2026
@cetagostini
cetagostini marked this pull request as ready for review July 28, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants