Skip to content

Commit

Permalink
Raise TypeError in dpnp.ndarray.__array__ (#2260)
Browse files Browse the repository at this point in the history
The PR implements `dpnp.ndarray.__array__` method that raises
`TypeError` to disallow implicit conversion of `dpnp.ndarray` to
`numpy.ndarray`.
While explicit conversion using `dpnp.ndarray.asnumpy` method is
advised.

Disallowing implicit conversion prevents `numpy.asarray(dpnp_arr)` from
creating an array of 0D `dpnp.ndarray` instances, because
using it is very costly due to multitude of short-array transfers from
GPU to host.
  • Loading branch information
antonwolfy authored Jan 21, 2025
1 parent 13816bd commit 356184a
Show file tree
Hide file tree
Showing 14 changed files with 65 additions and 28 deletions.
7 changes: 6 additions & 1 deletion dpnp/dpnp_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,12 @@ def __and__(self, other):
"""Return ``self&value``."""
return dpnp.bitwise_and(self, other)

# '__array__',
def __array__(self, dtype=None, /, *, copy=None):
raise TypeError(
"Implicit conversion to a NumPy array is not allowed. "
"Please use `.asnumpy()` to construct a NumPy array explicitly."
)

# '__array_finalize__',
# '__array_function__',
# '__array_interface__',
Expand Down
6 changes: 6 additions & 0 deletions dpnp/dpnp_iface_linearalgebra.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,12 @@ def einsum_path(*operands, optimize="greedy", einsum_call=False):
"""

# explicit casting to numpy array if applicable
operands = [
dpnp.asnumpy(x) if dpnp.is_supported_array_type(x) else x
for x in operands
]

return numpy.einsum_path(
*operands,
optimize=optimize,
Expand Down
8 changes: 6 additions & 2 deletions dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3207,10 +3207,14 @@ def roll(x, shift, axis=None):
[3, 4, 0, 1, 2]])
"""
if axis is None:
return roll(x.reshape(-1), shift, 0).reshape(x.shape)

usm_x = dpnp.get_usm_ndarray(x)
if dpnp.is_supported_array_type(shift):
shift = dpnp.asnumpy(shift)

if axis is None:
return roll(dpt.reshape(usm_x, -1), shift, 0).reshape(x.shape)

usm_res = dpt.roll(usm_x, shift=shift, axis=axis)
return dpnp_array._create_from_usm_ndarray(usm_res)

Expand Down
7 changes: 6 additions & 1 deletion dpnp/dpnp_utils/dpnp_utils_pad.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ def _as_pairs(x, ndim, as_index=False):
x = round(x)
return ((x, x),) * ndim

x = numpy.array(x)
# explicitly cast input "x" to NumPy array
if dpnp.is_supported_array_type(x):
x = dpnp.asnumpy(x)
else:
x = numpy.array(x)

if as_index:
x = numpy.asarray(numpy.round(x), dtype=numpy.intp)

Expand Down
2 changes: 2 additions & 0 deletions dpnp/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
from . import testing

numpy.testing.assert_allclose = testing.assert_allclose
numpy.testing.assert_almost_equal = testing.assert_almost_equal
numpy.testing.assert_array_almost_equal = testing.assert_array_almost_equal
numpy.testing.assert_array_equal = testing.assert_array_equal
numpy.testing.assert_equal = testing.assert_equal
26 changes: 15 additions & 11 deletions dpnp/tests/test_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
import dpnp
from dpnp.dpnp_array import dpnp_array

from .helper import get_all_dtypes, get_integer_dtypes, has_support_aspect64
from .helper import (
get_all_dtypes,
get_array,
get_integer_dtypes,
has_support_aspect64,
)
from .third_party.cupy import testing


Expand Down Expand Up @@ -441,16 +446,15 @@ class TestPut:
)
@pytest.mark.parametrize("ind_dt", get_all_dtypes(no_none=True))
@pytest.mark.parametrize(
"vals",
"ivals",
[0, [1, 2], (2, 2), dpnp.array([1, 2])],
ids=["0", "[1, 2]", "(2, 2)", "dpnp.array([1,2])"],
)
@pytest.mark.parametrize("mode", ["clip", "wrap"])
def test_input_1d(self, a_dt, indices, ind_dt, vals, mode):
def test_input_1d(self, a_dt, indices, ind_dt, ivals, mode):
a = numpy.array([-2, -1, 0, 1, 2], dtype=a_dt)
b = numpy.copy(a)
ia = dpnp.array(a)
ib = dpnp.array(b)
b, vals = numpy.copy(a), get_array(numpy, ivals)
ia, ib = dpnp.array(a), dpnp.array(b)

ind = numpy.array(indices, dtype=ind_dt)
if ind_dt == dpnp.bool and ind.all():
Expand All @@ -459,18 +463,18 @@ def test_input_1d(self, a_dt, indices, ind_dt, vals, mode):

if numpy.can_cast(ind_dt, numpy.intp, casting="safe"):
numpy.put(a, ind, vals, mode=mode)
dpnp.put(ia, iind, vals, mode=mode)
dpnp.put(ia, iind, ivals, mode=mode)
assert_array_equal(ia, a)

b.put(ind, vals, mode=mode)
ib.put(iind, vals, mode=mode)
ib.put(iind, ivals, mode=mode)
assert_array_equal(ib, b)
else:
assert_raises(TypeError, numpy.put, a, ind, vals, mode=mode)
assert_raises(TypeError, dpnp.put, ia, iind, vals, mode=mode)
assert_raises(TypeError, dpnp.put, ia, iind, ivals, mode=mode)

assert_raises(TypeError, b.put, ind, vals, mode=mode)
assert_raises(TypeError, ib.put, iind, vals, mode=mode)
assert_raises(TypeError, ib.put, iind, ivals, mode=mode)

@pytest.mark.parametrize("a_dt", get_all_dtypes(no_none=True))
@pytest.mark.parametrize(
Expand Down Expand Up @@ -637,7 +641,7 @@ def test_values(self, arr_dt, idx_dt, ndim, values):
ia, iind = dpnp.array(a), dpnp.array(ind)

for axis in range(ndim):
numpy.put_along_axis(a, ind, values, axis)
numpy.put_along_axis(a, ind, get_array(numpy, values), axis)
dpnp.put_along_axis(ia, iind, values, axis)
assert_array_equal(ia, a)

Expand Down
8 changes: 4 additions & 4 deletions dpnp/tests/test_random_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from dpnp.dpnp_array import dpnp_array
from dpnp.random import RandomState

from .helper import is_cpu_device
from .helper import get_array, is_cpu_device

# aspects of default device:
_def_device = dpctl.SyclQueue().sycl_device
Expand Down Expand Up @@ -224,7 +224,7 @@ def test_fallback(self, loc, scale):
# dpnp accepts only scalar as low and/or high, in other cases it will be a fallback to numpy
actual = data.asnumpy()
expected = numpy.random.RandomState(seed).normal(
loc=loc, scale=scale, size=size
loc=get_array(numpy, loc), scale=get_array(numpy, scale), size=size
)

dtype = get_default_floating()
Expand Down Expand Up @@ -557,7 +557,7 @@ def test_bounds_fallback(self, low, high):
RandomState(seed).randint(low=low, high=high, size=size).asnumpy()
)
expected = numpy.random.RandomState(seed).randint(
low=low, high=high, size=size
low=get_array(numpy, low), high=get_array(numpy, high), size=size
)
assert_equal(actual, expected)

Expand Down Expand Up @@ -1139,7 +1139,7 @@ def test_fallback(self, low, high):
# dpnp accepts only scalar as low and/or high, in other cases it will be a fallback to numpy
actual = data.asnumpy()
expected = numpy.random.RandomState(seed).uniform(
low=low, high=high, size=size
low=get_array(numpy, low), high=get_array(numpy, high), size=size
)

dtype = get_default_floating()
Expand Down
2 changes: 1 addition & 1 deletion dpnp/tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def test_ndim(self):
assert_array_equal(np_res, dpnp_res)

np_res = numpy.where(c, a.T, b.T)
dpnp_res = numpy.where(ic, ia.T, ib.T)
dpnp_res = dpnp.where(ic, ia.T, ib.T)
assert_array_equal(np_res, dpnp_res)

def test_dtype_mix(self):
Expand Down
2 changes: 2 additions & 0 deletions dpnp/tests/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .array import (
assert_allclose,
assert_almost_equal,
assert_array_almost_equal,
assert_array_equal,
assert_equal,
)
10 changes: 10 additions & 0 deletions dpnp/tests/testing/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from dpnp.dpnp_utils import convert_item

assert_allclose_orig = numpy.testing.assert_allclose
assert_almost_equal_orig = numpy.testing.assert_almost_equal
assert_array_almost_equal_orig = numpy.testing.assert_array_almost_equal
assert_array_equal_orig = numpy.testing.assert_array_equal
assert_equal_orig = numpy.testing.assert_equal

Expand All @@ -44,6 +46,14 @@ def assert_allclose(result, expected, *args, **kwargs):
_assert(assert_allclose_orig, result, expected, *args, **kwargs)


def assert_almost_equal(result, expected, *args, **kwargs):
_assert(assert_almost_equal_orig, result, expected, *args, **kwargs)


def assert_array_almost_equal(result, expected, *args, **kwargs):
_assert(assert_array_almost_equal_orig, result, expected, *args, **kwargs)


def assert_array_equal(result, expected, *args, **kwargs):
_assert(assert_array_equal_orig, result, expected, *args, **kwargs)

Expand Down
1 change: 0 additions & 1 deletion dpnp/tests/third_party/cupy/core_tests/test_ndarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,6 @@ def test_format(self, xp):
return format(x, ".2f")


@pytest.mark.skip("implicit conversation to numpy does not raise an exception")
class TestNdarrayImplicitConversion(unittest.TestCase):

def test_array(self):
Expand Down
4 changes: 2 additions & 2 deletions dpnp/tests/third_party/cupy/math_tests/test_sumprod.py
Original file line number Diff line number Diff line change
Expand Up @@ -1037,17 +1037,17 @@ def test_gradient_invalid_spacings1(self):
def test_gradient_invalid_spacings2(self):
# wrong length array in spacing
shape = (32, 16)
spacing = (15, cupy.arange(shape[1] + 1))
for xp in [numpy, cupy]:
spacing = (15, xp.arange(shape[1] + 1))
x = testing.shaped_random(shape, xp)
with pytest.raises(ValueError):
xp.gradient(x, *spacing)

def test_gradient_invalid_spacings3(self):
# spacing array with ndim != 1
shape = (32, 16)
spacing = (15, cupy.arange(shape[0]).reshape(4, -1))
for xp in [numpy, cupy]:
spacing = (15, xp.arange(shape[0]).reshape(4, -1))
x = testing.shaped_random(shape, xp)
with pytest.raises(ValueError):
xp.gradient(x, *spacing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class TestPermutationSoundness(unittest.TestCase):

def setUp(self):
a = cupy.random.permutation(self.num)
self.a = a
self.a = a.asnumpy()

# Test soundness

Expand Down
8 changes: 4 additions & 4 deletions dpnp/tests/third_party/cupy/random_tests/test_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ def test_bound_float2(self):
def test_goodness_of_fit(self):
mx = 5
trial = 100
vals = [random.randint(mx) for _ in range(trial)]
vals = [random.randint(mx).asnumpy() for _ in range(trial)]
counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
expected = numpy.array([float(trial) / mx] * mx)
assert _hypothesis.chi_square_test(counts, expected)

@_condition.repeat(3, 10)
def test_goodness_of_fit_2(self):
mx = 5
vals = random.randint(mx, size=(5, 20))
vals = random.randint(mx, size=(5, 20)).asnumpy()
counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
expected = numpy.array([float(vals.size) / mx] * mx)
assert _hypothesis.chi_square_test(counts, expected)
Expand Down Expand Up @@ -191,15 +191,15 @@ def test_bound_2(self):
def test_goodness_of_fit(self):
mx = 5
trial = 100
vals = [random.randint(0, mx) for _ in range(trial)]
vals = [random.randint(0, mx).asnumpy() for _ in range(trial)]
counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
expected = numpy.array([float(trial) / mx] * mx)
assert _hypothesis.chi_square_test(counts, expected)

@_condition.repeat(3, 10)
def test_goodness_of_fit_2(self):
mx = 5
vals = random.randint(0, mx, (5, 20))
vals = random.randint(0, mx, (5, 20)).asnumpy()
counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
expected = numpy.array([float(vals.size) / mx] * mx)
assert _hypothesis.chi_square_test(counts, expected)
Expand Down

0 comments on commit 356184a

Please sign in to comment.