Skip to content

Commit 7c8e83f

Browse files
authored
Align with dpctl changes for DLPack v1.0 support (#1980)
* Adopt dpnp to DLPack v1.0 * Add more tests to cover different use cases
1 parent ef4a310 commit 7c8e83f

File tree

4 files changed

+268
-11
lines changed

4 files changed

+268
-11
lines changed

dpnp/dpnp_array.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,27 +184,61 @@ def __copy__(self):
184184
# '__divmod__',
185185
# '__doc__',
186186

187-
def __dlpack__(self, stream=None):
187+
def __dlpack__(
188+
self, *, stream=None, max_version=None, dl_device=None, copy=None
189+
):
188190
"""
189191
Produces DLPack capsule.
190192
191193
Parameters
192194
----------
193195
stream : {:class:`dpctl.SyclQueue`, None}, optional
194-
Execution queue to synchronize with. If ``None``,
195-
synchronization is not performed.
196+
Execution queue to synchronize with. If ``None``, synchronization
197+
is not performed.
198+
Default: ``None``.
199+
max_version {tuple of ints, None}, optional
200+
The maximum DLPack version the consumer (caller of ``__dlpack__``)
201+
supports. As ``__dlpack__`` may not always return a DLPack capsule
202+
with version `max_version`, the consumer must verify the version
203+
even if this argument is passed.
204+
Default: ``None``.
205+
dl_device {tuple, None}, optional:
206+
The device the returned DLPack capsule will be placed on. The
207+
device must be a 2-tuple matching the format of
208+
``__dlpack_device__`` method, an integer enumerator representing
209+
the device type followed by an integer representing the index of
210+
the device.
211+
Default: ``None``.
212+
copy {bool, None}, optional:
213+
Boolean indicating whether or not to copy the input.
214+
215+
* If `copy` is ``True``, the input will always be copied.
216+
* If ``False``, a ``BufferError`` will be raised if a copy is
217+
deemed necessary.
218+
* If ``None``, a copy will be made only if deemed necessary,
219+
otherwise, the existing memory buffer will be reused.
220+
221+
Default: ``None``.
196222
197223
Raises
198224
------
199-
MemoryError
225+
MemoryError:
200226
when host memory can not be allocated.
201-
DLPackCreationError
202-
when array is allocated on a partitioned
203-
SYCL device, or with a non-default context.
227+
DLPackCreationError:
228+
when array is allocated on a partitioned SYCL device, or with
229+
a non-default context.
230+
BufferError:
231+
when a copy is deemed necessary but `copy` is ``False`` or when
232+
the provided `dl_device` cannot be handled.
204233
205234
"""
206235

207-
return self._array_obj.__dlpack__(stream=stream)
236+
return self._array_obj.__dlpack__(
237+
stream=stream,
238+
max_version=max_version,
239+
dl_device=dl_device,
240+
copy=copy,
241+
)
208242

209243
def __dlpack_device__(self):
210244
"""

dpnp/dpnp_iface.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ def default_float_type(device=None, sycl_queue=None):
464464
return map_dtype_to_device(float64, _sycl_queue.sycl_device)
465465

466466

467-
def from_dlpack(obj, /):
467+
def from_dlpack(obj, /, *, device=None, copy=None):
468468
"""
469469
Create a dpnp array from a Python object implementing the ``__dlpack__``
470470
protocol.
@@ -476,17 +476,46 @@ def from_dlpack(obj, /):
476476
obj : object
477477
A Python object representing an array that implements the ``__dlpack__``
478478
and ``__dlpack_device__`` methods.
479+
device : {:class:`dpctl.SyclDevice`, :class:`dpctl.SyclQueue`,
480+
:class:`dpctl.tensor.Device`, tuple, None}, optional
481+
Array API concept of a device where the output array is to be placed.
482+
``device`` can be ``None``, an oneAPI filter selector string,
483+
an instance of :class:`dpctl.SyclDevice` corresponding to
484+
a non-partitioned SYCL device, an instance of :class:`dpctl.SyclQueue`,
485+
a :class:`dpctl.tensor.Device` object returned by
486+
:attr:`dpctl.tensor.usm_ndarray.device`, or a 2-tuple matching
487+
the format of the output of the ``__dlpack_device__`` method,
488+
an integer enumerator representing the device type followed by
489+
an integer representing the index of the device.
490+
Default: ``None``.
491+
copy {bool, None}, optional
492+
Boolean indicating whether or not to copy the input.
493+
494+
* If `copy``is ``True``, the input will always be copied.
495+
* If ``False``, a ``BufferError`` will be raised if a copy is deemed
496+
necessary.
497+
* If ``None``, a copy will be made only if deemed necessary, otherwise,
498+
the existing memory buffer will be reused.
499+
500+
Default: ``None``.
479501
480502
Returns
481503
-------
482504
out : dpnp_array
483505
Returns a new dpnp array containing the data from another array
484506
(obj) with the ``__dlpack__`` method on the same device as object.
485507
508+
Raises
509+
------
510+
TypeError:
511+
if `obj` does not implement ``__dlpack__`` method
512+
ValueError:
513+
if the input array resides on an unsupported device
514+
486515
"""
487516

488-
usm_ary = dpt.from_dlpack(obj)
489-
return dpnp_array._create_from_usm_ndarray(usm_ary)
517+
usm_res = dpt.from_dlpack(obj, device=device, copy=copy)
518+
return dpnp_array._create_from_usm_ndarray(usm_res)
490519

491520

492521
def get_dpnp_descriptor(

tests/test_dlpack.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import numpy
2+
import pytest
3+
from numpy.testing import assert_array_equal
4+
5+
import dpnp
6+
7+
from .helper import (
8+
get_all_dtypes,
9+
)
10+
11+
device_oneAPI = 14 # DLDeviceType.kDLOneAPI
12+
13+
14+
class TestDLPack:
15+
@pytest.mark.parametrize("stream", [None, 1])
16+
def test_stream(self, stream):
17+
x = dpnp.arange(5)
18+
x.__dlpack__(stream=stream)
19+
20+
@pytest.mark.parametrize("copy", [True, None, False])
21+
def test_copy(self, copy):
22+
x = dpnp.arange(5)
23+
x.__dlpack__(copy=copy)
24+
25+
def test_wrong_copy(self):
26+
x = dpnp.arange(5)
27+
x.__dlpack__(copy=dpnp.array([1, 2, 3]))
28+
29+
@pytest.mark.parametrize("xp", [dpnp, numpy])
30+
@pytest.mark.parametrize("dt", get_all_dtypes(no_none=True))
31+
def test_dtype_passthrough(self, xp, dt):
32+
x = xp.arange(5).astype(dt)
33+
y = xp.from_dlpack(x)
34+
35+
assert y.dtype == x.dtype
36+
assert_array_equal(x, y)
37+
38+
@pytest.mark.parametrize("xp", [dpnp, numpy])
39+
def test_non_contiguous(self, xp):
40+
x = xp.arange(25).reshape((5, 5))
41+
42+
y1 = x[0]
43+
assert_array_equal(y1, xp.from_dlpack(y1))
44+
45+
y2 = x[:, 0]
46+
assert_array_equal(y2, xp.from_dlpack(y2))
47+
48+
y3 = x[1, :]
49+
assert_array_equal(y3, xp.from_dlpack(y3))
50+
51+
y4 = x[1]
52+
assert_array_equal(y4, xp.from_dlpack(y4))
53+
54+
y5 = xp.diagonal(x).copy()
55+
assert_array_equal(y5, xp.from_dlpack(y5))
56+
57+
def test_device(self):
58+
x = dpnp.arange(5)
59+
assert x.__dlpack_device__()[0] == device_oneAPI
60+
y = dpnp.from_dlpack(x)
61+
assert y.__dlpack_device__()[0] == device_oneAPI
62+
z = y[::2]
63+
assert z.__dlpack_device__()[0] == device_oneAPI
64+
65+
def test_ndim0(self):
66+
x = dpnp.array(1.0)
67+
y = dpnp.from_dlpack(x)
68+
assert_array_equal(x, y)
69+
70+
def test_device(self):
71+
x = dpnp.arange(5)
72+
y = dpnp.from_dlpack(x, device=x.__dlpack_device__())
73+
assert x.device == y.device
74+
assert x.get_array()._pointer == y.get_array()._pointer
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import unittest
2+
3+
import dpctl
4+
import dpctl.tensor._dlpack as dlp
5+
import numpy
6+
import pytest
7+
8+
import dpnp as cupy
9+
from tests.third_party.cupy import testing
10+
11+
12+
def _gen_array(dtype, alloc_q=None):
13+
if cupy.issubdtype(dtype, numpy.unsignedinteger):
14+
array = cupy.random.randint(
15+
0, 10, size=(2, 3), sycl_queue=alloc_q
16+
).astype(dtype)
17+
elif cupy.issubdtype(dtype, cupy.integer):
18+
array = cupy.random.randint(
19+
-10, 10, size=(2, 3), sycl_queue=alloc_q
20+
).astype(dtype)
21+
elif cupy.issubdtype(dtype, cupy.floating):
22+
array = cupy.random.rand(2, 3, sycl_queue=alloc_q).astype(dtype)
23+
elif cupy.issubdtype(dtype, cupy.complexfloating):
24+
array = cupy.random.random((2, 3), sycl_queue=alloc_q).astype(dtype)
25+
elif dtype == cupy.bool_:
26+
array = cupy.random.randint(
27+
0, 2, size=(2, 3), sycl_queue=alloc_q
28+
).astype(cupy.bool_)
29+
else:
30+
assert False, f"unrecognized dtype: {dtype}"
31+
return array
32+
33+
34+
class TestDLPackConversion(unittest.TestCase):
35+
@testing.for_all_dtypes(no_bool=False)
36+
def test_conversion(self, dtype):
37+
orig_array = _gen_array(dtype)
38+
tensor = orig_array.__dlpack__()
39+
out_array = dlp.from_dlpack_capsule(tensor)
40+
testing.assert_array_equal(orig_array, out_array)
41+
assert orig_array.get_array()._pointer == out_array._pointer
42+
43+
44+
@testing.parameterize(*testing.product({"memory": ("device", "managed")}))
45+
class TestNewDLPackConversion(unittest.TestCase):
46+
def _get_stream(self, stream_name):
47+
if stream_name == "null":
48+
return dpctl.SyclQueue()
49+
return dpctl.SyclQueue()
50+
51+
@testing.for_all_dtypes(no_bool=False)
52+
def test_conversion(self, dtype):
53+
orig_array = _gen_array(dtype)
54+
out_array = cupy.from_dlpack(orig_array)
55+
testing.assert_array_equal(orig_array, out_array)
56+
assert orig_array.get_array()._pointer == out_array.get_array()._pointer
57+
58+
def test_stream(self):
59+
allowed_streams = ["null", True]
60+
61+
# stream order is automatically established via DLPack protocol
62+
for src_s in [self._get_stream(s) for s in allowed_streams]:
63+
for dst_s in [self._get_stream(s) for s in allowed_streams]:
64+
orig_array = _gen_array(cupy.float32, alloc_q=src_s)
65+
dltensor = orig_array.__dlpack__(stream=orig_array)
66+
67+
out_array = dlp.from_dlpack_capsule(dltensor)
68+
out_array = cupy.from_dlpack(out_array, device=dst_s)
69+
testing.assert_array_equal(orig_array, out_array)
70+
assert (
71+
orig_array.get_array()._pointer
72+
== out_array.get_array()._pointer
73+
)
74+
75+
76+
class TestDLTensorMemory(unittest.TestCase):
77+
# def setUp(self):
78+
# self.old_pool = cupy.get_default_memory_pool()
79+
# self.pool = cupy.cuda.MemoryPool()
80+
# cupy.cuda.set_allocator(self.pool.malloc)
81+
82+
# def tearDown(self):
83+
# self.pool.free_all_blocks()
84+
# cupy.cuda.set_allocator(self.old_pool.malloc)
85+
86+
def test_deleter(self):
87+
# memory is freed when tensor is deleted, as it's not consumed
88+
array = cupy.empty(10)
89+
tensor = array.__dlpack__()
90+
# str(tensor): <capsule object "dltensor" at 0x7f7c4c835330>
91+
assert '"dltensor"' in str(tensor)
92+
# assert self.pool.n_free_blocks() == 0
93+
# del array
94+
# assert self.pool.n_free_blocks() == 0
95+
# del tensor
96+
# assert self.pool.n_free_blocks() == 1
97+
98+
def test_deleter2(self):
99+
# memory is freed when array2 is deleted, as tensor is consumed
100+
array = cupy.empty(10)
101+
tensor = array.__dlpack__()
102+
assert '"dltensor"' in str(tensor)
103+
array2 = dlp.from_dlpack_capsule(tensor)
104+
assert '"used_dltensor"' in str(tensor)
105+
# assert self.pool.n_free_blocks() == 0
106+
# del array
107+
# assert self.pool.n_free_blocks() == 0
108+
# del array2
109+
# assert self.pool.n_free_blocks() == 1
110+
# del tensor
111+
# assert self.pool.n_free_blocks() == 1
112+
113+
def test_multiple_consumption_error(self):
114+
# Prevent segfault, see #3611
115+
array = cupy.empty(10)
116+
tensor = array.__dlpack__()
117+
array2 = dlp.from_dlpack_capsule(tensor)
118+
with pytest.raises(ValueError) as e:
119+
array3 = dlp.from_dlpack_capsule(tensor)
120+
assert "consumed multiple times" in str(e.value)

0 commit comments

Comments
 (0)