You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I don't remember specifically - I'd guess it was about how stats functions often rely on apply_along_axis to provide an axis argument or how the resampling tests use apply_along_axis if the user-specified statistic doesn't support an axis argument.
I can imagine this providing a speedup vs looping on some backends (even though it doesn't really with NumPy). If so, I think it would be valuable, and we'd use it.
Something that would make it much more useful is if the arr argument would support a tuple of arrays. Currently, we concatenate arrays along axis and then split each slice within func1d, but that's quite inefficient. (Dealing with higher dimensional core shapes is not as bad because we can typically reshape arr then undo in func1d.)
This seems useful indeed, and was also discussed in numpy/numpy#30726 (tl;dr it's needed here, nothing to be done in numpy).
There indeed isn't a PyTorch equivalent to call. It was discussed a number of times, e.g. in https://discuss.pytorch.org/t/apply-a-function-along-an-axis/130440. That implementation kinda works, but has some mismatches in signature and things like negative axis handling. A few quick iterations with Deepseek combining the suggestion from the PyTorch forum with the NumPy implementation gives (untested!):
defapply_along_axis(
func1d: Callable,
axis: int,
arr: torch.Tensor,
*args: Any,
**kwargs: Any
) ->torch.Tensor:
# Handle negative axisifaxis<0:
axis=arr.dim() +axisifaxis>=arr.dim():
raiseValueError(f"axis {axis} is out of bounds for tensor of dimension {arr.dim()}")
# Get slices along the axisslices=torch.unbind(arr, dim=axis)
# Apply function to each sliceresults= [func1d(slice, *args, **kwargs) forsliceinslices]
# Check if all results are tensorsifnotall(isinstance(r, torch.Tensor) forrinresults):
# Convert scalars to tensorsresults= [rifisinstance(r, torch.Tensor) elsetorch.tensor(r, dtype=arr.dtype)
forrinresults]
# Try to stack results (works if all results have same shape)try:
returntorch.stack(results, dim=axis)
exceptRuntimeError:
# Stacking failed - results have different shapes# Use concatenation with unsqueeze to add the axis dimension back# Check if we need to unsqueeze based on the dimension of resultsreturntorch.cat([
r.unsqueeze(axis) ifr.dim() ==arr.dim() -1elserforrinresults
], dim=axis)
That would need tests and docs, but is probably not too far off. The PyTorch logic can be much simpler than the NumPy one - no need for __array_wrap__/__array_function__ handling (nor __torch_function__ I'd say), and no array scalars either.
@lucascolley possibly yes. Although I've just paused work on my current project @glass-dev for the next 3 months, so might be a while before I'm back on it
A few quick iterations with Deepseek combining the suggestion from the PyTorch forum with the NumPy implementation gives (untested!):
I tried testing this implementation (using xp.unstack instead of unbind), and It's slightly incorrect. It works well with some func1d like lambda x: x + 1 but fails for something like func1d = xp.sum The reason for that is that xp.unstack(arr, axis=axis) splits the array into (N-1) dimensional slices by removing the specified axis. However, apply_along_axis is supposed to apply func1d to every 1-D slice obtained by fixing all the other indices and varying only along the chosen axis. As a result, the implementation processes the wrong slices for arrays with more than two dimensions, leading to incorrect output shapes and values.
E AssertionError:
E Arrays are not equal
E
E (shapes (3,), (2, 4) mismatch)
E ACTUAL: array([ 60, 92, 124])
E DESIRED: array([[12, 15, 18, 21],
E [48, 51, 54, 57]])
As a fix, what I did is:
move the axis to the end
flatten all the remaining dimensions so each row is a 1-D slice
apply the function to every 1-D slice
stack the results, reshape, and permute the dimensions back to their original order
All while keeping it Array API compatible. See:
defapply_along_axis(
func1d: Callable[..., Array],
axis: int,
arr: Array,
*args,
**kwargs
) ->Array:
"""Apply a function to 1-D slices along a given axis."""ifaxis<0:
axis+=arr.ndimifnot0<=axis<arr.ndim:
msg=f"axis {axis} is out of bounds for array of dimension {arr.ndim}"raiseValueError(msg)
ifkwargsisNone:
kwargs= {}
ifnotisinstance(args, tuple):
args= (args,)
xp=array_namespace(arr)
# Move the target axis to the end.perm=tuple(iforiinrange(arr.ndim) ifi!=axis) + (axis,)
arr=xp.permute_dims(arr, perm)
leading_shape=arr.shape[:-1]
axis_len=arr.shape[-1]
arr=xp.reshape(arr, (-1, axis_len))
results= [func1d(slice, *args, **kwargs) forsliceinxp.unstack(arr, axis=0)]
ifnotall(hasattr(result, "dtype") forresultinresults):
results= [xp.asarray(result, dtype=arr.dtype) forresultinresults]
out=xp.stack(results, axis=0)
# Restore the leading dimensions.out=xp.reshape(out, leading_shape+out.shape[1:])
# Move the result dimensions back to the position of the original axis.before=tuple(range(axis))
after=tuple(range(axis, len(leading_shape)))
res=tuple(range(len(leading_shape), out.ndim))
perm=before+res+afterreturnxp.permute_dims(out, perm)
The above test passes with the changes. This needs some cleaning (chopping out too many variables, blank lines, and comments that AI added), but I guess this logic is a bit more robust.
Activity
lucascolley commentedon Oct 21, 2025
@mdhaber I think I remember some past discussion on this? Maybe I'm getting mixed up with something else.
mdhaber commentedon Oct 21, 2025
I don't remember specifically - I'd guess it was about how
statsfunctions often rely onapply_along_axisto provide anaxisargument or how the resampling tests useapply_along_axisif the user-specified statistic doesn't support anaxisargument.I can imagine this providing a speedup vs looping on some backends (even though it doesn't really with NumPy). If so, I think it would be valuable, and we'd use it.
Something that would make it much more useful is if the
arrargument would support a tuple of arrays. Currently, we concatenate arrays alongaxisand then split each slice withinfunc1d, but that's quite inefficient. (Dealing with higher dimensional core shapes is not as bad because we can typically reshapearrthen undo infunc1d.)rgommers commentedon Jan 30, 2026
This seems useful indeed, and was also discussed in numpy/numpy#30726 (tl;dr it's needed here, nothing to be done in
numpy).There indeed isn't a PyTorch equivalent to call. It was discussed a number of times, e.g. in https://discuss.pytorch.org/t/apply-a-function-along-an-axis/130440. That implementation kinda works, but has some mismatches in signature and things like negative axis handling. A few quick iterations with Deepseek combining the suggestion from the PyTorch forum with the NumPy implementation gives (untested!):
That would need tests and docs, but is probably not too far off. The PyTorch logic can be much simpler than the NumPy one - no need for
__array_wrap__/__array_function__handling (nor__torch_function__I'd say), and no array scalars either.lucascolley commentedon Jan 30, 2026
how close is that to being definable in terms of the standard? Looks pretty close, can we do
unbindalready?rgommers commentedon Jan 30, 2026
Ah good point.
unstackis very close tounbind:They seem to raise different errors for
axisout of bounds, but that should not matter too much.lucascolley commentedon Jan 30, 2026
@paddyroddy might you be interested in contributing an array-agnostic version here?
paddyroddy commentedon Jan 31, 2026
@lucascolley possibly yes. Although I've just paused work on my current project @glass-dev for the next 3 months, so might be a while before I'm back on it
prady0t commentedon Jul 27, 2026
I tried testing this implementation (using
xp.unstackinstead of unbind), and It's slightly incorrect. It works well with somefunc1dlikelambda x: x + 1but fails for something likefunc1d = xp.sumThe reason for that is thatxp.unstack(arr, axis=axis)splits the array into (N-1) dimensional slices by removing the specified axis. However,apply_along_axisis supposed to applyfunc1dto every 1-D slice obtained by fixing all the other indices and varying only along the chosen axis. As a result, the implementation processes the wrong slices for arrays with more than two dimensions, leading to incorrect output shapes and values.For example this test fails:
As a fix, what I did is:
All while keeping it Array API compatible. See:
The above test passes with the changes. This needs some cleaning (chopping out too many variables, blank lines, and comments that AI added), but I guess this logic is a bit more robust.
I'll open a PR to discuss this further.
[-]ENH: add apply_along_axis[/-][+]ENH: add `apply_along_axis`[/+]