Repository: scikit-learn/scikit-learn
File: sklearn/utils/_array_api.py (Python)

Code before:

    Parameters
    ----------
    *arrays : iterable of arrays
        Arrays to (potentially) move.

    xp_reference : namespace
        Array API namespace to move arrays to.

    device_reference : device
        Array API device to move arrays to.

    Returns
    -------
    arrays : list
        Arrays with the same namespace and device as reference.
    """
    sparse_mask = [sp.issparse(array) for array in arrays]
    none_mask = [array is None for array in arrays]
    if any(sparse_mask) and not _is_numpy_namespace(xp_reference):
        raise TypeError(
            "Sparse arrays are only supported when all dense arrays are Numpy arrays."
        )
    # Early return if all `arrays` are sparse or None (to numpy)
    if all(
        sparse_mask[i] or none_mask[i] for i in range(len(arrays))
    ) and _is_numpy_namespace(xp_reference):
        return arrays

    converted_arrays = []

    for array, is_sparse, is_none in zip(arrays, sparse_mask, none_mask):
        if is_none:
            converted_arrays.append(None)
        elif is_sparse:
            converted_arrays.append(array)
        else:
            xp_array, _, device_array = get_namespace_and_device(array)
            if xp_reference == xp_array and device_reference == device_array:
                converted_arrays.append(array)
            else:
                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 2023.12. Namespace libraries
                    # should only trigger a copy automatically if needed.
                    array_converted = xp_reference.from_dlpack(
                        array, device=device_reference
                    )

Review comment (bug):
I wonder if we want to get rid of the `except` block now by simply using the newest pytorch version. I think we haven't defined any constraints in the documentation of which pytorch min version we support, array API is experimental and in the CI we only test array API on the newest versions.

Currently, in the CUDA CI we use the latest pytorch version available on conda-forge (2.8), but we could equally use the latest version available on pypi (2.9) if that makes things easier (which is how it looks to me with the CUDA CI failure). I suspect we would move to 2.9 in a few months anyways and the perspective of having array api available in non-experimental mode one day is years away.

Background: pytorch has DLPack version 1.0 support since version [2.9](https://github.com/pytorch/pytorch/releases) (after https://github.com/pytorch/pytorch/pull/145000 has been merged). 

Code after:
        elif is_sparse:
            converted_arrays.append(array)
        else:
            xp_array, _, device_array = get_namespace_and_device(array)
            if xp == xp_array and device == device_array:
                converted_arrays.append(array)
            else:
                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)
                    # `AttributeError` occurs when `__dlpack__` and `__dlpack_device__`
                    # methods are not present on the input array
                    # `TypeError` and `NotImplementedError` for packages that do not
                    # yet support dlpack 1.0
                    # (i.e. the `device`/`copy` kwargs, e.g., torch <= 2.8.0)
                    # See https://github.com/data-apis/array-api/pull/741 for
                    # more details about the introduction of the `copy` and `device`
                    # kwargs in the from_dlpack method and their expected
                    # meaning by namespaces implementing the array API spec.
                    # TODO: try removing this once DLPack v1 more widely supported
                except (AttributeError, TypeError, NotImplementedError):
                    # Converting to numpy is tricky, handle this via dedicated function
                    if _is_numpy_namespace(xp):
                        array_converted = _convert_to_numpy(array, xp_array)
                    # Convert from numpy, all array libraries can do this
                    elif _is_numpy_namespace(xp_array):
                        array_converted = xp.asarray(array, device=device)
                    else:
                        # There is no generic way to convert from namespace A to B
                        # So we first convert from A to numpy and then from numpy to B
                        # The way to avoid this round trip is to lobby for DLpack
                        # support in libraries A and B
                        array_np = _convert_to_numpy(array, xp_array)
                        array_converted = xp.asarray(array_np, device=device)
                converted_arrays.append(array_converted)

    return (
        converted_arrays[0] if len(converted_arrays) == 1 else tuple(converted_arrays)
    )


def _expit(X, xp=None):
    xp, _ = get_namespace(X, xp=xp)
    if _is_numpy_namespace(xp):
        return xp.asarray(special.expit(numpy.asarray(X)))