The move_to function we have for moving arrays from one namespace to another can crash the Python process when you ask it to move an array with negative strides. I found this while working on PCA where we have an array with negative stride (Vt from _fit_truncated).
The code below is a reproducer:
import numpy as np
import torch
import sklearn
from sklearn.utils._array_api import get_namespace_and_device, move_to
def make_negative_strided():
"""A small, contiguous-source array viewed with a negative stride."""
a = np.arange(12.0).reshape(3, 4)[:, ::-1]
assert not a.flags["C_CONTIGUOUS"]
assert a.strides[-1] < 0
return a
a = make_negative_strided()
with sklearn.config_context(array_api_dispatch=True):
xp, _, device = get_namespace_and_device(torch.asarray([1.0]))
try:
move_to(a, xp=xp, device=device)
except Exception as exc: # it is not caught
print("Caught a Python exception (unexpected):", type(exc).__name__, exc)
else:
# this doesn't happen
print("move_to returned without error (unexpected)")
# instead the process exits with a SIGABRT
AI tells me that a possible fix is to call np.ascontiguousarray(array) if the input array is an array with negative stride. In addition we should only apply this workaround in the case where the move_to target is torch. The comment for the workaround should also include a link to pytorch/pytorch#188023 and this issue so we can know when the work around can be removed.
Something like this, but with a check for torch as target:
--- a/sklearn/utils/_array_api.py
+++ b/sklearn/utils/_array_api.py
@@ def move_to(*arrays, xp, device):
xp_array, _, device_array = get_namespace_and_device(array)
if xp == xp_array and device == device_array:
converted_arrays.append(array)
else:
+ strides = getattr(array, "strides", None)
+ if strides is not None and any(
+ stride < 0 for stride in strides
+ ):
+ # Some libraries (e.g. torch) do not raise a catchable
+ # Python exception but abort the whole process when a NumPy
+ # array with negative or zero strides is imported via DLPack
+ # (such arrays are produced for instance by ``a[::-1]``
+ # reversals, as done by ARPACK). The ``except`` clause below
+ # therefore cannot protect against it, so the source array is
+ # made contiguous first to guarantee positive strides.
+ array = numpy.ascontiguousarray(array)
try:
# The dlpack protocol is the future proof and library agnostic
# method to transfer arrays across namespace and device boundaries
# hence this method is attempted first and going through NumPy is
# only used as fallback in case of failure.
# Note: copy=None is the default since array-api 2023.12. Namespace
# libraries should only trigger a copy automatically if needed.
array_converted = xp.from_dlpack(array, device=device)
What do others think about this? I am also wondering if this is a bug in numpy's DLPack implementation (should not expose arrays with negative stride) or if this is a bug in torch (should check for negative stride before doing anything)?
I think that numpy.ascontiguousarray(array) is free for most arrays, those that already full fill it. For arrays that are not already contiguous I don't know what the cost is, but I think there is no alternative?
The
move_tofunction we have for moving arrays from one namespace to another can crash the Python process when you ask it to move an array with negative strides. I found this while working onPCAwhere we have an array with negative stride (Vtfrom_fit_truncated).The code below is a reproducer:
AI tells me that a possible fix is to call
np.ascontiguousarray(array)if the inputarrayis an array with negative stride. In addition we should only apply this workaround in the case where themove_totarget is torch. The comment for the workaround should also include a link to pytorch/pytorch#188023 and this issue so we can know when the work around can be removed.Something like this, but with a check for torch as target:
What do others think about this? I am also wondering if this is a bug in numpy's DLPack implementation (should not expose arrays with negative stride) or if this is a bug in torch (should check for negative stride before doing anything)?
I think that
numpy.ascontiguousarray(array)is free for most arrays, those that already full fill it. For arrays that are not already contiguous I don't know what the cost is, but I think there is no alternative?