Automatically move `y` (and `sample_weight`) to the same device and namespace as `X`

(From https://github.com/scikit-learn/scikit-learn/pull/27800#issuecomment-1878709518 by @ogrisel)

The proposal/idea is to allow `y` to not be on the same device (and namespace?) as `X` when using Array API inputs. Currently we require/assume that they are on the same device and namespace, it is a requirement. However pipelines can't modify `y` which means it is not possible to move from CPU to GPU as one of the steps of the pipeline, the whole pipeline has to stay on one device. The below details some example and code to motivate allowing `X` ad `y` being on different devices (and namespaces).

---

Suppose we have:

```python
>>> import torch
>>> from sklearn import set_config
>>> from sklearn.datasets import make_regression
>>> from sklearn.linear_model import Ridge
>>> set_config(array_api_dispatch=True)
>>> X, y = make_regression(n_samples=int(1e5), n_features=int(1e3), random_state=0)
>>> X_torch_cuda = torch.tensor(X).to("cuda")
>>> y_torch_cuda = torch.tensor(y).to("cuda")
```

I did a quick benchmark with timeit on a host with a 32 cores CPU and an A100 GPU: we get a bit more than 10x speed-up (which is in the range of what I would have expected):

```python
>>> %time Ridge(solver="svd").fit(X, y)
CPU times: user 1min 29s, sys: 1min 4s, total: 2min 34s
Wall time: 6.18 s
Ridge(solver='svd')
>>> %time Ridge(solver="svd").fit(X_torch_cuda, y_torch_cuda)
CPU times: user 398 ms, sys: 2.74 ms, total: 401 ms
Wall time: 402 ms
Ridge(solver='svd')
```

I also tried the following:

```python
>>> Ridge(solver="svd").fit(X_torch_cuda, y)
Traceback (most recent call last):
  Cell In[36], line 1
    Ridge(solver="svd").fit(X_torch_cuda, y)
  File ~/code/scikit-learn/sklearn/base.py:1194 in wrapper
    return fit_method(estimator, *args, **kwargs)
  File ~/code/scikit-learn/sklearn/linear_model/_ridge.py:1197 in fit
    device_ = device(*input_arrays)
  File ~/code/scikit-learn/sklearn/utils/_array_api.py:104 in device
    raise ValueError("Input arrays use different devices.")
ValueError: Input arrays use different devices.
```

I think it might be reasonable to expect this pattern to fail in general. explicitly ask the the user to provide inputs with consistently allocated data buffers.

However, we might want to be more lenient for the particular case of `y` (and `sample_weight`) and change `Ridge.fit` to automatically move `y` to the same namespace and device as `X`:

```python
y = xp.asarray(y, device=device(X))
```

The reason would be to improve the usability of the Array API for the following pipelines:


```python
X_pandas_df, y_pandas_series = fetch_some_pandas_data()

pipeline = make_pipeline(
    some_column_transformer(),  # works on CPU on the input dataframe
    FunctionTransformer(func=lambda X: torch.tensor(X).to("float32").to("cuda")),
    Ridge(solver="svd"),
)
pipeline.fit(X_pandas_df, y_pandas_series)
```

The pipeline steps can only transform `X` and not `y` (or `sample_weight`). So it means that user would have to instead call:

```python
pipeline.fit(X_pandas_df, torch.tensor(y_pandas_series).to("cuda"))
```

which feels a bit weird/cumbersome to me.

This might not be a big deal though and I don't want to delay this PR to get a consensus on this particular point: I think it's fine the way it is for now but we might want come back to the UX of pipelines with Array API steps later. I will open a dedicated issue.

---

If we take action there are a few estimators that need double checking. For example `LinearDiscriminantAnalysis` currently raises low level PyTorch exceptions when `X` and `y` are on different devices.

COMMENT:
Just wanted to summarise (including what was discussed last meeting):

* `y` and `sample_weight` following `X` - required; it's otherwise not possible to implement pipelines where the namespace of X changes across steps (for instance via a FunctionTransformer that moves extracted numerical features to pytorch on GPU after a first few steps that work on pandas inputs on GPU).
* hyperparameters that are arrays following `X` - this is more debatable but I think consensus was that this would be nice as well

How this is move done (ref: https://github.com/scikit-learn/scikit-learn/issues/28668#issuecomment-2009805011):

* numpy arrays and Python lists are easy to move to the same namespace/device
* arrays that are on a different namespace/device combo:
   * try [`~array_api.from_dlpack`](https://github.com/dmlc/dlpack/blob/main/docs/source/python_spec.rst#id3) (@ogrisel you mention that "there is no guarantee that cross-device transfers work in general" - the DLPack [docs](https://dmlc.github.io/dlpack/latest/python_spec.html#syntax-for-data-interchange-with-dlpack) say that `from_dlpack` will accept "any (array) object with the two DLPack methods ([__dlpack__()](https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__dlpack__.html#array_api.array.__dlpack__) and [__dlpack_device__()](https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__dlpack_device__.html#array_api.array.__dlpack_device__)) implemented" - is that what you meant, or even if the 2 methods are implemented, there is still no guarantee of successful transfer?)
   * if above does not work, fallback on internal `_convert_to_numpy` to convert to numpy and then `xp.asarray` : i.e. do a namespace_origin -> numpy -> namespace_destination conversion.

<details>

<summary>compat/strict `from_dlpack` error</summary>

With reference to your comment on `from_dlpack not yet being implemented on `array-api-compat`/`array-api-strict`, I am currently getting a different error when running e.g.:

```python
>>> import array_api_compat.torch as xp_torch
>>> import array_api_compat.numpy as xp_np
>>> xp_np.from_dlpack(xp_torch.ones(3, device="mps"))
```

Error:

> NotImplementedError: Could not run 'aten::empty.memory_format' with arguments from the 'MPS' backend. This could be because the operator doesn't exist for this backend, or was omitted during the selective/custom build process (if using custom build). If you are a Facebook employee using PyTorch on mobile, please visit https://fburl.com/ptmfixes for possible resolutions. 'aten::empty.memory_format' is only available for these backends: [CPU, Meta, QuantizedCPU, QuantizedMeta, MkldnnCPU, SparseCPU, SparseMeta, SparseCsrCPU, SparseCsrMeta, BackendSelect, Python, FuncTorchDynamicLayerBackMode, Functionalize, Named, Conjugate, Negative, ZeroTensor, ADInplaceOrView, AutogradOther, AutogradCPU, AutogradCUDA, AutogradHIP, AutogradXLA, AutogradMPS, AutogradIPU, AutogradXPU, AutogradHPU, AutogradVE, AutogradLazy, AutogradMTIA, AutogradPrivateUse1, AutogradPrivateUse2, AutogradPrivateUse3, AutogradMeta, AutogradNestedTensor, Tracer, AutocastCPU, AutocastXPU, AutocastMPS, AutocastCUDA, FuncTorchBatched, BatchedNestedTensor, FuncTorchVmapMode, Batched, VmapMode, FuncTorchGradWrapper, PythonTLSSnapshot, FuncTorchDynamicLayerFrontMode, PreDispatch, PythonDispatcher].

Interesting that it specifically mentions Facebook employees..?

</details>

I think this is slightly out of scope for this issue but we also have the issue of whether `y_true` should follow `y_pred` (ref: https://github.com/scikit-learn/scikit-learn/pull/30439#issuecomment-2532238196) - should we create a new issue for this?

Labels: Array API