Repository navigation
MLX: lower index_copy into a mutated user input functionally #23508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5654233
70c4e3b
ac4df62
78f44d4
5ff5435
ed87ac7
40ee446
f06ccfb
4a1ff49
9803564
d0878d7
03fc902
64e84e0
ec2a265
f4c81be
f628e6c
57c6196
24d820e
f7b5334
961cf20
7235b99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,6 +94,7 @@ | |
| GreaterEqualNode, | ||
| GreaterNode, | ||
| IdCopyNode, | ||
| IndexCopyNode, | ||
| IntOrVid, | ||
| IntOrVidOrTid, | ||
| ItemIntNode, | ||
|
|
@@ -2406,6 +2407,38 @@ def _roll_handler(P: MLXProgramBuilder, n: Node) -> Slot: | |
| return out | ||
|
|
||
|
|
||
| @REGISTRY.register(target=[torch.ops.aten.index_copy.default]) | ||
| def _index_copy_handler(P: MLXProgramBuilder, n: Node) -> Slot: | ||
| """index_copy into a new tensor, leaving `dst` as it was. | ||
|
|
||
| The INDEX_COPY pattern writes in place when `dst` is a buffer the method | ||
| mutates; anything else, a mutated user input included, lands here and the | ||
| caller writes the result back. | ||
| """ | ||
| args = P.args(n) | ||
| require_args(args, 4, 4, "aten.index_copy.default") | ||
| require_kwargs(P.kwargs(n), set(), "aten.index_copy.default") | ||
| dst, axis, indices, update = args | ||
| require_static_int(axis, "dim", "aten.index_copy.default") | ||
| # The runtime takes a 1-D index into a tensor with at least one dim; ATen also | ||
| # allows 0-d ones, which stay off the delegate. | ||
| if n.args[0].meta["val"].dim() == 0 or n.args[2].meta["val"].dim() != 1: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The source tensor can also be scalar so either the interpreter needs to support scalar tensors or we have to reject them here. |
||
| raise NotImplementedError( | ||
| "aten.index_copy.default needs a 1-D index into a tensor of rank >= 1" | ||
| ) | ||
| out = P.make_or_get_slot(n) | ||
| P.emit( | ||
| IndexCopyNode( | ||
| dst=P.slot_to_tid(dst), | ||
| update=P.slot_to_tid(update), | ||
| indices=P.slot_to_tid(indices), | ||
| out=P.slot_to_tid(out), | ||
| axis=axis, | ||
| ) | ||
| ) | ||
| return out | ||
|
|
||
|
|
||
| @REGISTRY.register(target=[torch.ops.aten.index.Tensor]) | ||
| def _index_handler(P: MLXProgramBuilder, n: Node) -> Slot: | ||
| args = P.args(n) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1308,7 +1308,11 @@ inline std::tuple<int, int, int, int> next_contiguous_run( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Optimizes into slice_update calls for contiguous runs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inline void | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exec_index_copy(const IndexCopyNode& n, ExecutionState& st, StreamOrDevice s) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| array& dst = st.tensor_ref(n.dst); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // When out == dst, use direct assignment to preserve MLX buffer donation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only then is dst written, so a functional update may read a constant. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bool in_place = (n.out.idx == n.dst.idx); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const array& dst = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| in_place ? st.tensor_ref(n.dst) : st.const_tensor_ref(n.dst); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const array& upd = st.const_tensor_ref(n.update); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const array& indices = st.const_tensor_ref(n.indices); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (indices.ndim() != 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1319,26 +1323,31 @@ exec_index_copy(const IndexCopyNode& n, ExecutionState& st, StreamOrDevice s) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const size_t uaxis = static_cast<size_t>(axis); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const int dst_dim = static_cast<int>(dst.shape()[uaxis]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get indices as a vector of ints, handling negative indices | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Note: PyTorch uses int64 for indices, so we read as int64_t | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eval(indices); // Ensure indices are materialized before accessing data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (indices.dtype() != ::mlx::core::int64) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Like ATen, negative indices are rejected rather than wrapped. Export | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // accepts int32 indices too, so they are widened rather than rejected. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Indices computed in the graph can be a strided view, so they are made | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // contiguous before their data is read. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const array idx_arr = contiguous( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indices.dtype() == ::mlx::core::int32 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? astype(indices, ::mlx::core::int64, s) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : indices, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eval(idx_arr); // Ensure indices are materialized before accessing data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1330
to
+1336
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This adds a GPU sync to every
executorch/backends/mlx/runtime/MLXInterpreter.h Lines 341 to 370 in 7235b99
eval the tensor as-is, check flags().row_contiguous, and read int32/int64 on the host with a switch instead of astype.
Could you factor that into a shared helper both ops use? Something like: // Reads a 1-D int32/int64 tensor on the host. Free for a materialized input;
// only indices computed in the graph cost a sync.
inline std::vector<int64_t>
read_host_indices(array a, const char* op, StreamOrDevice s) {
eval(a);
if (!a.flags().row_contiguous) {
a = contiguous(a, false, s);
eval(a);
}
// switch on a.dtype(): copy int32 / int64 out, throw otherwise
}The contiguous fallback keeps strided indices working; only indices computed in the graph can be strided, and those already need a sync. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (idx_arr.dtype() != ::mlx::core::int64) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw std::invalid_argument( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| std::string("IndexCopyNode: expected int64 indices, got ") + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| std::string("IndexCopyNode: expected int64 or int32 indices, got ") + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExecutionState::dtype_str(indices.dtype())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| std::vector<int32_t> idx_vec(indices.size()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto idx_data = indices.data<int64_t>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < indices.size(); ++i) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int64_t idx = idx_data[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (idx < 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| idx += dst_dim; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| std::vector<int32_t> idx_vec(idx_arr.size()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto idx_data = idx_arr.data<int64_t>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < idx_arr.size(); ++i) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const int64_t idx = idx_data[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (idx < 0 || idx >= dst_dim) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw std::out_of_range( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "IndexCopyNode: index " + std::to_string(idx_data[i]) + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| " out of range for axis " + std::to_string(axis) + " with size " + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| std::to_string(dst_dim)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "index_copy: index " + std::to_string(idx) + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| " is out of bounds for dimension " + std::to_string(axis) + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| " with size " + std::to_string(dst_dim)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (idx > std::numeric_limits<int32_t>::max()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw std::out_of_range( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1348,11 +1357,9 @@ exec_index_copy(const IndexCopyNode& n, ExecutionState& st, StreamOrDevice s) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| idx_vec[i] = static_cast<int>(idx); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // When out == dst, use direct assignment to preserve MLX buffer donation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: I'm not sure if this is needed as a special case since the standard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // st.set_tensor does a std::move. Keeping for now, but should investigate and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // possibly remove in future. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bool in_place = (n.out.idx == n.dst.idx); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: I'm not sure the in-place case is needed as a special case since the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // standard st.set_tensor does a std::move. Keeping for now, but should | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // investigate and possibly remove in future. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (idx_vec.empty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!in_place) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1402,8 +1409,9 @@ exec_index_copy(const IndexCopyNode& n, ExecutionState& st, StreamOrDevice s) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : slice(upd, to_shape(upd_vstart), to_shape(upd_vstop), s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (in_place) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dst = slice_update( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dst, upd_slice, to_shape(dst_vstart), to_shape(dst_vstop), s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| array& target = st.tensor_ref(n.dst); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target = slice_update( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target, upd_slice, to_shape(dst_vstart), to_shape(dst_vstop), s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = slice_update( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result, upd_slice, to_shape(dst_vstart), to_shape(dst_vstop), s); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.