Skip to content

Honour idx_list in the MLX AdvancedIncSubtensor dispatch - #2397

Open
guillaume-osmo wants to merge 1 commit into
pymc-devs:mainfrom
guillaume-osmo:fix-mlx-advanced-inc-subtensor
Open

guillaume-osmo wants to merge 1 commit into
pymc-devs:mainfrom
guillaume-osmo:fix-mlx-advanced-inc-subtensor

Conversation

@guillaume-osmo

Copy link
Copy Markdown
Contributor

Closes #2382. Closes #2387.

Motivation

Both issues are the same bug. mlx_funcify_AdvancedIncSubtensor passed the flat index list straight to the scatter, ignoring op.idx_list. idx_list records where the index variables sit among plain slices, so dropping it anchors the advanced indices at axis 0 and shifts every one of them left. Any destination with leading batch axes then fails to broadcast:

issue idx_list error
#2382 (slice(None), 0) [broadcast_shapes] Shapes (4,1) and (7,3) cannot be broadcast
#2387 (slice(None), 0, 1) [broadcast_shapes] Shapes (5,3) and (3,3) cannot be broadcast

Both are easy to hit without writing any indexing, since specialize rewrites diagonal(...) into this form — the gradient of a log-determinant over a batch of matrices was enough.

Implementation

Call unflatten_index_variables, mirroring AdvancedIncSubtensor.perform. This is also what the neighbouring AdvancedSubtensor dispatch already does (via indices_from_subtensor).

The set_instead_of_inc and ignore_duplicates branches take the same path, and MLX accepts a slice inside the index tuple for all three (x.at[(slice(None), i, j)].add(y) works).

Tests

Four added; all four fail without the change:

Duplicate-index accumulation is unchanged (verified against np.add.at).

Full tests/link/mlx/ suite: no regressions.

, pymc-devs#2387)

`mlx_funcify_AdvancedIncSubtensor` passed the flat index list straight to
the scatter, ignoring `op.idx_list`. `idx_list` records where the index
variables sit among plain slices, so dropping it anchored the advanced
indices at axis 0 and shifted every one of them left. Any destination with
leading batch axes then failed to broadcast:

    idx_list=(slice(None), 0)       -> pymc-devs#2382, [broadcast_shapes] (4,1) vs (7,3)
    idx_list=(slice(None), 0, 1)    -> pymc-devs#2387, [broadcast_shapes] (5,3) vs (3,3)

Both are easy to hit without writing any indexing, since `specialize`
rewrites `diagonal(...)` into this form -- the gradient of a log-determinant
over a batch of matrices was enough.

Call `unflatten_index_variables`, mirroring `AdvancedIncSubtensor.perform`,
which is also what the `AdvancedSubtensor` dispatch already does. The
`set_instead_of_inc` and `ignore_duplicates` branches take the same path,
and MLX accepts a slice in the index tuple for all three.

@jessegrabowski jessegrabowski 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.

Fix is right, it's the piece of db7fa07 that never reached the mlx dispatch. Two things before merge: slices with bounds still fail because the bounds arrive as mx arrays (pytensor/link/mlx/dispatch/subtensor.py:110), and the four tests are one parametrized test (tests/link/mlx/test_subtensor.py:352). Full mlx suite passes locally.

return x.at[indices].add(y)

def advancedincsubtensor(x, y, *ilist, mlx_fn=mlx_fn):
idx_list = op.idx_list

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.

inline this into the signature like incsubtensor above

op._check_runtime_broadcast_of_vector_index(node, x, y, ilist[0])

return mlx_fn(x, ilist, y)
# `idx_list` records where the index variables sit among plain slices,

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.

drop this comment, it's a changelog. # mirrors AdvancedIncSubtensor.perform is plenty

# the flat `ilist` directly shifted every index left, which broke any
# graph whose destination carries leading batch axes (#2382, #2387).
# This mirrors `AdvancedIncSubtensor.perform`.
indices = unflatten_index_variables(ilist, idx_list)

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.

slice bounds come back as 0-d mx arrays here, so x[1:4, idx] still raises. cast the slice components to int (AdvancedSubtensor above has the same bug, factor out a helper and use it in both)

# the flat `ilist` directly shifted every index left, which broke any
# graph whose destination carries leading batch axes (#2382, #2387).
# This mirrors `AdvancedIncSubtensor.perform`.
indices = unflatten_index_variables(ilist, idx_list)

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.

use indices_from_subtensor like the rest of the file

compare_mlx_and_py([x], [out], [np.zeros(3, dtype=np.float32)])


def test_mlx_AdvancedIncSubtensor_leading_slice():

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.

collapse the four new tests into one parametrized over inc/set and index form, like test_mlx_AdvancedIncSubtensor1_duplicate_indices below. add x[idx, :] and x[1:4, idx] cases, and a x[1:4, idx] case to test_mlx_AdvancedSubtensor for the read side

assert isinstance(op, pt_subtensor.AdvancedIncSubtensor)
assert op.idx_list == (slice(None, None, None), 0, 1)

compare_mlx_and_py([x], [out], [np.zeros((5, 3, 3), dtype="float32")])

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.

zeros with a constant y can't tell set from inc. use arange

compare_mlx_and_py([x], [out], [np.zeros((5, 3, 3), dtype="float32")])


def test_mlx_AdvancedIncSubtensor_grad_batched_diagonal():

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.

this is the same graph as the test above, remove

compare_mlx_and_py([x], [out], [np.zeros((5, 3, 3), dtype="float32")])


def test_mlx_AdvancedIncSubtensor_vectorized_scatter():

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.

remove, x[:, idx] in the parametrized test hits the same idx_list. whether specialize collapses the Blockwise isn't an mlx concern


def test_mlx_AdvancedIncSubtensor_vectorized_scatter():
"""A scatter-add that ``vectorize_graph`` has given a leading batch dim (#2382)."""
from pytensor.graph.replace import vectorize_graph

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.

module level

[batched],
[out],
[np.ones((n_batch, n_levels), dtype="float32")],
mlx_mode="MLX",

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.

pass a Mode

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.

MLX: gradient of batched advanced indexing fails in broadcast_shapes MLX AdvancedIncSubtensor fails when Blockwise adds a batch dim

2 participants