Skip to content

Bound gather_nd / scatter_nd indices by the dims they actually address - #2806

Open
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:gather-nd-validate-indices
Open

Bound gather_nd / scatter_nd indices by the dims they actually address#2806
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:gather-nd-validate-indices

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

iOS17 gather_nd and scatter_nd validate indices against the full input shape:

upper_bound = self.x.shape
if np.count_nonzero(np.logical_or(indices < 0, indices >= upper_bound)):

indices has shape (*K) and its last axis addresses only indices.shape[-1] dims, starting after batch_dims for gather_nd. Comparing a (..., K) array against a length rank(x) tuple only lines up when K == rank(x).

Two in-bounds programs that fail to convert today:

# x is rank 3, index depth is 2
mb.gather_nd(x=np.arange(24, dtype=np.float32).reshape(2, 3, 4),
             indices=np.array([[0, 1], [1, 2]], dtype=np.int32),
             validate_indices=True)
# ValueError: operands could not be broadcast together with shapes (2,2) (3,)

# batch_dims=1, so index 2 addresses x.shape[1], which is 3 long
mb.gather_nd(x=np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32),
             indices=np.array([[1], [2]], dtype=np.int32),
             batch_dims=1, validate_indices=True)
# IndexError: Indices is out of bounds ... Expected indices between [0, (2, 3))

The second is the K == 1 case, where the comparison broadcasts and then checks every index against every dim.

Fix: slice the shape down to the dims the indices address — data.shape[:K] for scatter_nd, x.shape[batch_dims : batch_dims + K] for gather_nd. Symbolic dims are skipped, since they cannot be compared. Out-of-bounds indices are still rejected, now against the right bound.

Three new tests in TestScatterNd and TestGatherNd, all failing on main. The existing validate_indices tests are unchanged and still pass.

iOS17 gather_nd and scatter_nd validate indices against the whole input
shape:

    upper_bound = self.x.shape
    if np.count_nonzero(np.logical_or(indices < 0, indices >= upper_bound)):

indices has shape (*K) and its last axis addresses only indices.shape[-1]
dims, starting after batch_dims for gather_nd. Comparing a (..., K) array
with a length rank(x) tuple only lines up when K == rank(x). Otherwise it
either fails to broadcast, or, when K is 1, compares every index against
every dim of x.

Both make in-bounds programs fail to convert. gather_nd on a rank 3 x with
indices of depth 2 raises "operands could not be broadcast together with
shapes (2,2) (3,)", and gather_nd with batch_dims=1 on a (2, 3) x rejects
index 2 for being >= x.shape[0], although it addresses x.shape[1].

Slice the shape down to the dims the indices address. Symbolic dims are
skipped, since they cannot be compared.
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.

1 participant