Skip to content

Explicit shape comparison for dpnp and numpy outputs #2295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ This release achieves 100% compliance with Python Array API specification (revis
* Updated `dpnp.einsum` to add support for `order=None` [#2411](https://github.com/IntelPython/dpnp/pull/2411)
* Updated Python Array API specification version supported to `2024.12` [#2416](https://github.com/IntelPython/dpnp/pull/2416)
* Removed `einsum_call` keyword from `dpnp.einsum_path` signature [#2421](https://github.com/IntelPython/dpnp/pull/2421)
* Updated `dpnp.vdot` to return a 0-D array when one of the inputs is a scalar [#2295](https://github.com/IntelPython/dpnp/pull/2295)
* Updated `dpnp.outer` to return the same dtype as NumPy when multiplying an array with a scalar [#2295](https://github.com/IntelPython/dpnp/pull/2295)

### Fixed

Expand Down
35 changes: 24 additions & 11 deletions dpnp/dpnp_iface_linearalgebra.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,30 @@


# TODO: implement a specific scalar-array kernel
def _call_multiply(a, b, out=None):
"""Call multiply function for special cases of scalar-array dots."""
def _call_multiply(a, b, out=None, outer_calc=False):
"""
Adjusted multiply function for handling special cases of scalar-array dot
products in linear algebra.

`dpnp.multiply` cannot directly be used for calculating scalar-array dots,
because the output dtype of multiply is not the same as the expected dtype
for scalar-array dots. For example, if `sc` is an scalar and `a` is an
array of type `float32`, then `dpnp.multiply(a, sc).dtype == dpnp.float32`
(similar to NumPy). However, for scalar-array dots, such as the dot
function, we need `dpnp.dot(a, sc).dtype == dpnp.float64` to align with
NumPy. This functions adjusts the behavior of `dpnp.multiply` function to
meet this requirement.

"""

sc, arr = (a, b) if dpnp.isscalar(a) else (b, a)
sc_dtype = map_dtype_to_device(type(sc), arr.sycl_device)
res_dtype = dpnp.result_type(sc_dtype, arr)
multiply_func = dpnp.multiply.outer if outer_calc else dpnp.multiply
if out is not None and out.dtype == arr.dtype:
res = dpnp.multiply(a, b, out=out)
res = multiply_func(a, b, out=out)
else:
res = dpnp.multiply(a, b, dtype=res_dtype)
res = multiply_func(a, b, dtype=res_dtype)
return dpnp.get_result_array(res, out, casting="no")


Expand Down Expand Up @@ -1109,16 +1123,15 @@ def outer(a, b, out=None):

dpnp.check_supported_arrays_type(a, b, scalar_type=True, all_scalars=False)
if dpnp.isscalar(a):
x1 = a
x2 = dpnp.ravel(b)[None, :]
result = _call_multiply(a, x2, out=out, outer_calc=True)
elif dpnp.isscalar(b):
x1 = dpnp.ravel(a)[:, None]
x2 = b
result = _call_multiply(x1, b, out=out, outer_calc=True)
else:
x1 = dpnp.ravel(a)
x2 = dpnp.ravel(b)
result = dpnp.multiply.outer(dpnp.ravel(a), dpnp.ravel(b), out=out)

return dpnp.multiply.outer(x1, x2, out=out)
return result


def tensordot(a, b, axes=2):
Expand Down Expand Up @@ -1288,13 +1301,13 @@ def vdot(a, b):
if b.size != 1:
raise ValueError("The second array should be of size one.")
a_conj = numpy.conj(a)
return _call_multiply(a_conj, b)
return dpnp.squeeze(_call_multiply(a_conj, b))

if dpnp.isscalar(b):
if a.size != 1:
raise ValueError("The first array should be of size one.")
a_conj = dpnp.conj(a)
return _call_multiply(a_conj, b)
return dpnp.squeeze(_call_multiply(a_conj, b))

if a.ndim == 1 and b.ndim == 1:
return dpnp_dot(a, b, out=None, conjugate=True)
Expand Down
2 changes: 1 addition & 1 deletion dpnp/dpnp_utils/dpnp_utils_linearalgebra.py
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ def dpnp_multiplication(
result = dpnp.moveaxis(result, (-2, -1), axes_res)
elif len(axes_res) == 1:
result = dpnp.moveaxis(result, (-1,), axes_res)
return dpnp.ascontiguousarray(result)
return result

return dpnp.asarray(result, order=order)

Expand Down
41 changes: 29 additions & 12 deletions dpnp/tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,29 @@
from . import config


def _assert_dtype(a_dt, b_dt, check_only_type_kind=False):
if check_only_type_kind:
assert a_dt.kind == b_dt.kind, f"{a_dt.kind} != {b_dt.kind}"
else:
assert a_dt == b_dt, f"{a_dt} != {b_dt}"


def _assert_shape(a, b):
# it is assumed `a` is a `dpnp.ndarray` and so it has shape attribute
if hasattr(b, "shape"):
assert a.shape == b.shape, f"{a.shape} != {b.shape}"
else:
# numpy output is scalar, then dpnp is 0-D array
assert a.shape == (), f"{a.shape} != ()"


def assert_dtype_allclose(
dpnp_arr,
numpy_arr,
check_type=True,
check_only_type_kind=False,
factor=8,
relative_factor=None,
check_shape=True,
):
"""
Assert DPNP and NumPy array based on maximum dtype resolution of input arrays
Expand All @@ -37,10 +53,13 @@ def assert_dtype_allclose(
for all data types supported by DPNP when set to True.
It is effective only when 'check_type' is also set to True.
The parameter `factor` scales the resolution used for comparing the arrays.
The parameter `check_shape`, when True (default), asserts the shape of input arrays is the same.

"""

list_64bit_types = [numpy.float64, numpy.complex128]
if check_shape:
_assert_shape(dpnp_arr, numpy_arr)

is_inexact = lambda x: hasattr(x, "dtype") and dpnp.issubdtype(
x.dtype, dpnp.inexact
)
Expand All @@ -57,34 +76,32 @@ def assert_dtype_allclose(
else -dpnp.inf
)
tol = factor * max(tol_dpnp, tol_numpy)
assert_allclose(dpnp_arr.asnumpy(), numpy_arr, atol=tol, rtol=tol)
assert_allclose(dpnp_arr, numpy_arr, atol=tol, rtol=tol, strict=False)
if check_type:
list_64bit_types = [numpy.float64, numpy.complex128]
numpy_arr_dtype = numpy_arr.dtype
dpnp_arr_dtype = dpnp_arr.dtype
dpnp_arr_dev = dpnp_arr.sycl_device

if check_only_type_kind:
assert dpnp_arr_dtype.kind == numpy_arr_dtype.kind
_assert_dtype(dpnp_arr_dtype, numpy_arr_dtype, True)
else:
is_np_arr_f2 = numpy_arr_dtype == numpy.float16

if is_np_arr_f2:
if has_support_aspect16(dpnp_arr_dev):
assert dpnp_arr_dtype == numpy_arr_dtype
_assert_dtype(dpnp_arr_dtype, numpy_arr_dtype)
elif (
numpy_arr_dtype not in list_64bit_types
or has_support_aspect64(dpnp_arr_dev)
):
assert dpnp_arr_dtype == numpy_arr_dtype
_assert_dtype(dpnp_arr_dtype, numpy_arr_dtype)
else:
assert dpnp_arr_dtype.kind == numpy_arr_dtype.kind
_assert_dtype(dpnp_arr_dtype, numpy_arr_dtype, True)
else:
assert_array_equal(dpnp_arr.asnumpy(), numpy_arr)
assert_array_equal(dpnp_arr, numpy_arr, strict=False)
if check_type and hasattr(numpy_arr, "dtype"):
if check_only_type_kind:
assert dpnp_arr.dtype.kind == numpy_arr.dtype.kind
else:
assert dpnp_arr.dtype == numpy_arr.dtype
_assert_dtype(dpnp_arr.dtype, numpy_arr.dtype, check_only_type_kind)


def generate_random_numpy_array(
Expand Down
4 changes: 0 additions & 4 deletions dpnp/tests/test_arraycreation.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,15 +952,13 @@ def test_ascontiguousarray1(data):
result = dpnp.ascontiguousarray(data)
expected = numpy.ascontiguousarray(data)
assert_dtype_allclose(result, expected)
assert result.shape == expected.shape


@pytest.mark.parametrize("data", [(), 1, (2, 3), [4]])
def test_ascontiguousarray2(data):
result = dpnp.ascontiguousarray(dpnp.array(data))
expected = numpy.ascontiguousarray(numpy.array(data))
assert_dtype_allclose(result, expected)
assert result.shape == expected.shape


@pytest.mark.parametrize(
Expand All @@ -970,15 +968,13 @@ def test_asfortranarray1(data):
result = dpnp.asfortranarray(data)
expected = numpy.asfortranarray(data)
assert_dtype_allclose(result, expected)
assert result.shape == expected.shape


@pytest.mark.parametrize("data", [(), 1, (2, 3), [4]])
def test_asfortranarray2(data):
result = dpnp.asfortranarray(dpnp.array(data))
expected = numpy.asfortranarray(numpy.array(data))
assert_dtype_allclose(result, expected)
assert result.shape == expected.shape


def test_meshgrid_raise_error():
Expand Down
11 changes: 4 additions & 7 deletions dpnp/tests/test_arraypad.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def test_basic(self, mode):
result = dpnp.pad(a_dp, (25, 20), mode=mode)
if mode == "empty":
# omit uninitialized "empty" boundary from the comparison
assert result.shape == expected.shape
assert_equal(result[25:-20], expected[25:-20])
else:
assert_array_equal(result, expected)
Expand Down Expand Up @@ -70,7 +69,6 @@ def test_non_contiguous_array(self, mode):
result = dpnp.pad(a_dp, (2, 3), mode=mode)
if mode == "empty":
# omit uninitialized "empty" boundary from the comparison
assert result.shape == expected.shape
assert_equal(result[2:-3, 2:-3], expected[2:-3, 2:-3])
else:
assert_array_equal(result, expected)
Expand Down Expand Up @@ -287,10 +285,10 @@ def test_linear_ramp_end_values(self):
"""Ensure that end values are exact."""
a_dp = dpnp.ones(10).reshape(2, 5)
a = dpnp.pad(a_dp, (223, 123), mode="linear_ramp")
assert_equal(a[:, 0], 0.0)
assert_equal(a[:, -1], 0.0)
assert_equal(a[0, :], 0.0)
assert_equal(a[-1, :], 0.0)
assert_equal(a[:, 0], 0.0, strict=False)
assert_equal(a[:, -1], 0.0, strict=False)
assert_equal(a[0, :], 0.0, strict=False)
assert_equal(a[-1, :], 0.0, strict=False)

@pytest.mark.parametrize(
"dtype", [numpy.uint32, numpy.uint64] + get_all_dtypes(no_none=True)
Expand Down Expand Up @@ -426,7 +424,6 @@ def test_empty(self):
expected = numpy.pad(a_np, [(2, 3), (3, 1)], "empty")
result = dpnp.pad(a_dp, [(2, 3), (3, 1)], "empty")
# omit uninitialized "empty" boundary from the comparison
assert result.shape == expected.shape
assert_equal(result[2:-3, 3:-1], expected[2:-3, 3:-1])

# Check how padding behaves on arrays with an empty dimension.
Expand Down
1 change: 0 additions & 1 deletion dpnp/tests/test_dlpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ def test_dtype_passthrough(self, xp, dt):
x = xp.arange(5).astype(dt)
y = xp.from_dlpack(x)

assert y.dtype == x.dtype
assert_array_equal(x, y)

@pytest.mark.parametrize("xp", [dpnp, numpy])
Expand Down
14 changes: 7 additions & 7 deletions dpnp/tests/test_fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def test_fill_strided_array():
expected = dpnp.tile(dpnp.asarray([0, 1], dtype=a.dtype), 50)

b.fill(1)
assert_array_equal(b, 1)
assert_array_equal(b, 1, strict=False)
assert_array_equal(a, expected)


Expand All @@ -51,7 +51,7 @@ def test_fill_strided_2d_array(order):
expected[::-2, ::2] = 1

b.fill(1)
assert_array_equal(b, 1)
assert_array_equal(b, 1, strict=False)
assert_array_equal(a, expected)


Expand All @@ -60,27 +60,27 @@ def test_fill_memset(order):
a = dpnp.ones((10, 10), dtype="i4", order=order)
a.fill(0)

assert_array_equal(a, 0)
assert_array_equal(a, 0, strict=False)


def test_fill_float_complex_to_int():
a = dpnp.ones((10, 10), dtype="i4")

a.fill(complex(2, 0))
assert_array_equal(a, 2)
assert_array_equal(a, 2, strict=False)

a.fill(float(3))
assert_array_equal(a, 3)
assert_array_equal(a, 3, strict=False)


def test_fill_complex_to_float():
a = dpnp.ones((10, 10), dtype="f4")

a.fill(complex(2, 0))
assert_array_equal(a, 2)
assert_array_equal(a, 2, strict=False)


def test_fill_bool():
a = dpnp.full(5, fill_value=7, dtype="i4")
a.fill(True)
assert_array_equal(a, 1)
assert_array_equal(a, 1, strict=False)
Loading
Loading