Skip to content

Commit 933a4a7

Browse files
authored
Improve DLPack-compatible array imports (#3495)
1 parent f7f5040 commit 933a4a7

9 files changed

Lines changed: 45 additions & 22 deletions

File tree

docs/src/usage/numpy.rst

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,22 +75,16 @@ even though no in-place operations on MLX memory are executed.
7575
PyTorch
7676
-------
7777

78-
.. warning::
79-
80-
PyTorch Support for :obj:`memoryview` is experimental and can break for
81-
multi-dimensional arrays. Casting to NumPy first is advised for now.
82-
83-
PyTorch supports the buffer protocol, but it requires an explicit
84-
:obj:`memoryview`.
78+
PyTorch supports DLPack inputs and can import MLX arrays directly.
8579

8680
.. code-block:: python
8781
8882
import mlx.core as mx
8983
import torch
9084
9185
a = mx.arange(3)
92-
b = torch.tensor(memoryview(a))
93-
c = mx.array(b)
86+
b = torch.tensor(a)
87+
c = mx.array(b.cpu())
9488
9589
JAX
9690
---

python/mlx/_stub_patterns.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
mlx.core.__prefix__:
2-
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, ParamSpec, TypeVar
2+
from typing import Any, Callable, Dict, List, Optional, Protocol, Sequence, Tuple, Union, ParamSpec, TypeVar
33
import sys
44
if sys.version_info >= (3, 10):
55
from typing import TypeAlias
66
else:
77
from typing_extensions import TypeAlias
88
P = ParamSpec("P")
99
R = TypeVar("R")
10+
class DLPackCompatible(Protocol):
11+
__dlpack__: Callable[..., Any]
12+
__dlpack_device__: Callable[..., Any]
1013

1114
mlx.core.__suffix__:
1215
from typing import Union

python/src/array.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ void init_array(nb::module_& m) {
300300
"val"_a,
301301
"dtype"_a = nb::none(),
302302
nb::sig(
303-
"def __init__(self: array, val: Union[scalar, list, tuple, numpy.ndarray, array], dtype: Optional[Dtype] = None)"))
303+
"def __init__(self: array, val: Union[scalar, list, tuple, DLPackCompatible, array], dtype: Optional[Dtype] = None)"))
304304
.def_prop_ro(
305305
"size",
306306
&mx::array::size,
@@ -479,7 +479,7 @@ void init_array(nb::module_& m) {
479479
throw std::invalid_argument(
480480
"Invalid pickle state: expected (ndarray, Dtype::Val)");
481481
}
482-
using ND = nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu>;
482+
using ND = nb::ndarray<nb::ro, nb::c_contig>;
483483
ND nd = nb::cast<ND>(state[0]);
484484
auto val = static_cast<mx::Dtype::Val>(nb::cast<uint8_t>(state[1]));
485485
if (val == mx::Dtype::Val::bfloat16) {

python/src/convert.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ int check_shape_dim(int64_t dim) {
3333

3434
template <typename T>
3535
mx::array nd_array_to_mlx_contiguous(
36-
nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu> nd_array,
36+
nb::ndarray<nb::ro, nb::c_contig> nd_array,
3737
const mx::Shape& shape,
3838
mx::Dtype dtype) {
3939
// Make a copy of the numpy buffer
@@ -43,9 +43,14 @@ mx::array nd_array_to_mlx_contiguous(
4343
}
4444

4545
mx::array nd_array_to_mlx(
46-
nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu> nd_array,
46+
nb::ndarray<nb::ro, nb::c_contig> nd_array,
4747
std::optional<mx::Dtype> dtype,
4848
std::optional<nb::dlpack::dtype> nb_dtype) {
49+
if (nd_array.device_type() != nb::device::cpu::value) {
50+
throw std::invalid_argument(
51+
"Cannot convert non-CPU DLPack array to mlx array.");
52+
}
53+
4954
// Compute the shape and size
5055
mx::Shape shape;
5156
shape.reserve(nd_array.ndim());
@@ -495,7 +500,7 @@ mx::array create_array(nb::object v, std::optional<mx::Dtype> t) {
495500
auto arr = nb::cast<mx::array>(v);
496501
return mx::astype(arr, t.value_or(arr.dtype()));
497502
} else if (nb::ndarray_check(v)) {
498-
using ContigArray = nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu>;
503+
using ContigArray = nb::ndarray<nb::ro, nb::c_contig>;
499504
ContigArray nd;
500505
std::optional<nb::dlpack::dtype> nb_dtype;
501506
// Nanobind does not recognize bfloat16 numpy array:

python/src/convert.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ struct ArrayLike {
6262
};
6363

6464
mx::array nd_array_to_mlx(
65-
nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu> nd_array,
65+
nb::ndarray<nb::ro, nb::c_contig> nd_array,
6666
std::optional<mx::Dtype> mx_dtype,
6767
std::optional<nb::dlpack::dtype> nb_dtype = std::nullopt);
6868

python/src/indexing.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ mlx_compute_slice_update_args(
926926
}
927927

928928
std::optional<mx::array> extract_boolean_mask(const nb::object& obj) {
929-
using NDArray = nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu>;
929+
using NDArray = nb::ndarray<nb::ro, nb::c_contig>;
930930
if (nb::isinstance<nb::bool_>(obj)) {
931931
return mx::array(nb::cast<bool>(obj), mx::bool_);
932932
} else if (nb::isinstance<mx::array>(obj)) {

python/src/utils.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ mx::array to_array(
3838
return mx::array(static_cast<mx::complex64_t>(*pv), mx::complex64);
3939
} else if (auto pv = std::get_if<mx::array>(&v); pv) {
4040
return *pv;
41-
} else if (auto pv = std::get_if<
42-
nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu>>(&v);
43-
pv) {
41+
} else if (auto pv = std::get_if<nb::ndarray<nb::ro, nb::c_contig>>(&v); pv) {
4442
return nd_array_to_mlx(*pv, dtype);
4543
} else {
4644
return to_array_with_accessor(std::get<ArrayLike>(v).obj);

python/src/utils.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ using ScalarOrArray = std::variant<
2424
// Must be above ndarray
2525
mx::array,
2626
// Must be above complex
27-
nb::ndarray<nb::ro, nb::c_contig, nb::device::cpu>,
27+
nb::ndarray<nb::ro, nb::c_contig>,
2828
std::complex<float>,
2929
ArrayLike>;
3030

python/tests/test_array.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,17 @@
2020
import tensorflow as tf
2121

2222
has_tf = True
23-
except ImportError as e:
23+
except ImportError:
2424
has_tf = False
2525

26+
try:
27+
import torch
28+
29+
has_torch_mps = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
30+
except ImportError:
31+
torch = None
32+
has_torch_mps = False
33+
2634

2735
class TestVersion(mlx_tests.MLXTestCase):
2836
def test_version(self):
@@ -2039,6 +2047,21 @@ def test_dlpack(self):
20392047
y = np.from_dlpack(x)
20402048
self.assertTrue(mx.array_equal(y, x))
20412049

2050+
@unittest.skipUnless(has_torch_mps, "PyTorch MPS is required")
2051+
def test_torch_mps_dlpack_non_cpu_error(self):
2052+
x = torch.arange(12, device="mps", dtype=torch.float32).reshape(3, 4)
2053+
self.assertEqual(x.__dlpack_device__()[0], 8)
2054+
2055+
with self.assertRaisesRegex(ValueError, "non-CPU DLPack"):
2056+
mx.array(x)
2057+
2058+
a = mx.array([1])
2059+
b = torch.tensor([2])
2060+
self.assertTrue(mx.array_equal(a + b, mx.array([3])))
2061+
2062+
with self.assertRaisesRegex(ValueError, "non-CPU DLPack"):
2063+
a + b.to("mps")
2064+
20422065
def test_getitem_with_list(self):
20432066
a = mx.array([1, 2, 3, 4, 5])
20442067
idx = [0, 2, 4]

0 commit comments

Comments
 (0)