diff --git a/CHANGELOG.md b/CHANGELOG.md index 96649123cfa..3e8f270168a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Also, that release drops support for Python 3.9, making Python 3.10 the minimum * Corrected a phonetic spelling issue due to incorrect using of `a nd` in docstrings [#2719](https://github.com/IntelPython/dpnp/pull/2719) * Resolved an issue causing `dpnp.linspace` to return an incorrect output shape when inputs were passed as arrays [#2712](https://github.com/IntelPython/dpnp/pull/2712) * Resolved an issue where `dpnp` always returns the base allocation pointer, when the view start is expected [#2651](https://github.com/IntelPython/dpnp/pull/2651) +* Fixed an issue causing an exception in `dpnp.geomspace` and `dpnp.logspace` when called with explicit `device` keyword but any input array is allocated on another device [#2723](https://github.com/IntelPython/dpnp/pull/2723) ### Security diff --git a/dpnp/dpnp_algo/dpnp_arraycreation.py b/dpnp/dpnp_algo/dpnp_arraycreation.py index 00958690609..a96d07bfecf 100644 --- a/dpnp/dpnp_algo/dpnp_arraycreation.py +++ b/dpnp/dpnp_algo/dpnp_arraycreation.py @@ -46,11 +46,46 @@ def _as_usm_ndarray(a, usm_type, sycl_queue): + """Converts input object to `dpctl.tensor.usm_ndarray`""" + if isinstance(a, dpnp_array): - return a.get_array() + a = a.get_array() return dpt.asarray(a, usm_type=usm_type, sycl_queue=sycl_queue) +def _check_has_zero_val(a): + """Check if any element in input object is equal to zero""" + + if dpnp.isscalar(a): + if a == 0: + return True + elif hasattr(a, "any"): + if (a == 0).any(): + return True + elif any(val == 0 for val in a): + return True + return False + + +def _get_usm_allocations(objs, device=None, usm_type=None, sycl_queue=None): + """ + Get common USM allocations based on a list of input objects and an explicit + device, a SYCL queue, or a USM type if specified. + + """ + + alloc_usm_type, alloc_sycl_queue = get_usm_allocations(objs) + + if sycl_queue is None and device is None: + sycl_queue = alloc_sycl_queue + + if usm_type is None: + usm_type = alloc_usm_type or "device" + return usm_type, dpnp.get_normalized_queue_device( + sycl_queue=sycl_queue, device=device + ) + + def dpnp_geomspace( start, stop, @@ -62,76 +97,57 @@ def dpnp_geomspace( endpoint=True, axis=0, ): - usm_type_alloc, sycl_queue_alloc = get_usm_allocations([start, stop]) - - if sycl_queue is None and device is None: - sycl_queue = sycl_queue_alloc - sycl_queue_normalized = dpnp.get_normalized_queue_device( - sycl_queue=sycl_queue, device=device + usm_type, sycl_queue = _get_usm_allocations( + [start, stop], device=device, usm_type=usm_type, sycl_queue=sycl_queue ) - if usm_type is None: - _usm_type = "device" if usm_type_alloc is None else usm_type_alloc - else: - _usm_type = usm_type + if _check_has_zero_val(start) or _check_has_zero_val(stop): + raise ValueError("Geometric sequence cannot include zero") - start = _as_usm_ndarray(start, _usm_type, sycl_queue_normalized) - stop = _as_usm_ndarray(stop, _usm_type, sycl_queue_normalized) + start = dpnp.array(start, usm_type=usm_type, sycl_queue=sycl_queue) + stop = dpnp.array(stop, usm_type=usm_type, sycl_queue=sycl_queue) dt = numpy.result_type(start, stop, float(num)) - dt = map_dtype_to_device(dt, sycl_queue_normalized.sycl_device) + dt = map_dtype_to_device(dt, sycl_queue.sycl_device) if dtype is None: dtype = dt - if dpnp.any(start == 0) or dpnp.any(stop == 0): - raise ValueError("Geometric sequence cannot include zero") + # promote both arguments to the same dtype + start = start.astype(dt, copy=False) + stop = stop.astype(dt, copy=False) - out_sign = dpt.ones( - dpt.broadcast_arrays(start, stop)[0].shape, - dtype=dt, - usm_type=_usm_type, - sycl_queue=sycl_queue_normalized, - ) - # Avoid negligible real or imaginary parts in output by rotating to - # positive real, calculating, then undoing rotation - if dpnp.issubdtype(dt, dpnp.complexfloating): - all_imag = (start.real == 0.0) & (stop.real == 0.0) - if dpnp.any(all_imag): - start[all_imag] = start[all_imag].imag - stop[all_imag] = stop[all_imag].imag - out_sign[all_imag] = 1j - - both_negative = (dpt.sign(start) == -1) & (dpt.sign(stop) == -1) - if dpnp.any(both_negative): - dpt.negative(start[both_negative], out=start[both_negative]) - dpt.negative(stop[both_negative], out=stop[both_negative]) - dpt.negative(out_sign[both_negative], out=out_sign[both_negative]) - - log_start = dpt.log10(start) - log_stop = dpt.log10(stop) + # Allow negative real values and ensure a consistent result for complex + # (including avoiding negligible real or imaginary parts in output) by + # rotating start to positive real, calculating, then undoing rotation. + out_sign = dpnp.sign(start) + start = start / out_sign + stop = stop / out_sign + + log_start = dpnp.log10(start) + log_stop = dpnp.log10(stop) res = dpnp_logspace( log_start, log_stop, num=num, endpoint=endpoint, base=10.0, - dtype=dtype, - usm_type=_usm_type, - sycl_queue=sycl_queue_normalized, - ).get_array() + dtype=dt, + usm_type=usm_type, + sycl_queue=sycl_queue, + ) + # Make sure the endpoints match the start and stop arguments. This is + # necessary because np.exp(np.log(x)) is not necessarily equal to x. if num > 0: res[0] = start if num > 1 and endpoint: res[-1] = stop - res = out_sign * res + res *= out_sign if axis != 0: - res = dpt.moveaxis(res, 0, axis) - - res = dpt.astype(res, dtype, copy=False) - return dpnp_array._create_from_usm_ndarray(res) + res = dpnp.moveaxis(res, 0, axis) + return res.astype(dtype, copy=False) def dpnp_linspace( @@ -264,45 +280,36 @@ def dpnp_logspace( dtype=None, axis=0, ): - if not dpnp.isscalar(base): - usm_type_alloc, sycl_queue_alloc = get_usm_allocations( - [start, stop, base] - ) - - if sycl_queue is None and device is None: - sycl_queue = sycl_queue_alloc - sycl_queue = dpnp.get_normalized_queue_device( - sycl_queue=sycl_queue, device=device - ) - - if usm_type is None: - usm_type = "device" if usm_type_alloc is None else usm_type_alloc - else: - usm_type = usm_type + usm_type, sycl_queue = _get_usm_allocations( + [start, stop, base], + device=device, + usm_type=usm_type, + sycl_queue=sycl_queue, + ) - start = _as_usm_ndarray(start, usm_type, sycl_queue) - stop = _as_usm_ndarray(stop, usm_type, sycl_queue) - base = _as_usm_ndarray(base, usm_type, sycl_queue) + if not dpnp.isscalar(base): + base = dpnp.array(base, usm_type=usm_type, sycl_queue=sycl_queue) + start = dpnp.array(start, usm_type=usm_type, sycl_queue=sycl_queue) + stop = dpnp.array(stop, usm_type=usm_type, sycl_queue=sycl_queue) - [start, stop, base] = dpt.broadcast_arrays(start, stop, base) - base = dpt.expand_dims(base, axis=axis) + start, stop, base = dpnp.broadcast_arrays(start, stop, base) + base = dpnp.expand_dims(base, axis=axis) - # assume res as not a tuple, because retstep is False + # assume `res` as not a tuple, because retstep is False res = dpnp_linspace( start, stop, num=num, - device=device, usm_type=usm_type, sycl_queue=sycl_queue, endpoint=endpoint, axis=axis, - ).get_array() + ) - dpt.pow(base, res, out=res) + dpnp.pow(base, res, out=res) if dtype is not None: - res = dpt.astype(res, dtype, copy=False) - return dpnp_array._create_from_usm_ndarray(res) + res = res.astype(dtype, copy=False) + return res class dpnp_nd_grid: diff --git a/dpnp/tests/test_arraycreation.py b/dpnp/tests/test_arraycreation.py index 65747488310..ce5a93c6e37 100644 --- a/dpnp/tests/test_arraycreation.py +++ b/dpnp/tests/test_arraycreation.py @@ -21,9 +21,6 @@ get_array, get_float_dtypes, has_support_aspect64, - is_lts_driver, - is_tgllp_iris_xe, - is_win_platform, ) from .third_party.cupy import testing @@ -85,6 +82,61 @@ def test_validate_positional_args(self, xp): ) +class TestGeomspace: + @pytest.mark.parametrize("sign", [-1, 1]) + @pytest.mark.parametrize("dtype", get_all_dtypes()) + @pytest.mark.parametrize("num", [2, 4, 8, 3, 9, 27]) + @pytest.mark.parametrize("endpoint", [True, False]) + def test_basic(self, sign, dtype, num, endpoint): + start = 2 * sign + stop = 127 * sign + + func = lambda xp: xp.geomspace( + start, stop, num, endpoint=endpoint, dtype=dtype + ) + + np_res = func(numpy) + dpnp_res = func(dpnp) + + assert_allclose(dpnp_res, np_res, rtol=1e-06) + + @pytest.mark.parametrize("start", [1j, 1 + 1j]) + @pytest.mark.parametrize("stop", [10j, 10 + 10j]) + def test_complex(self, start, stop): + func = lambda xp: xp.geomspace(start, stop, num=10) + np_res = func(numpy) + dpnp_res = func(dpnp) + assert_allclose(dpnp_res, np_res, rtol=1e-06) + + @pytest.mark.parametrize("axis", [0, 1]) + def test_axis(self, axis): + func = lambda xp: xp.geomspace([2, 3], [20, 15], num=10, axis=axis) + np_res = func(numpy) + dpnp_res = func(dpnp) + assert_allclose(dpnp_res, np_res, rtol=1e-06) + + def test_num_zero(self): + func = lambda xp: xp.geomspace(1, 10, num=0, endpoint=False) + np_res = func(numpy) + dpnp_res = func(dpnp) + assert_allclose(dpnp_res, np_res) + + @pytest.mark.parametrize( + "start, stop, num", + [ + (0, 5, 3), + (2, 0, 3), + (0, 0, 3), + (dpnp.array([0]), 7, 10), + (-2, numpy.array([[0]]), 7), + ([2, 4, 0], 3, 5), + ], + ) + def test_zero_error(self, start, stop, num): + with pytest.raises(ValueError): + dpnp.geomspace(start, stop, num) + + class TestLinspace: @pytest.mark.parametrize("start", [0, -5, 10, -2.5, 9.7]) @pytest.mark.parametrize("stop", [0, 10, -2, 20.5, 120]) @@ -210,6 +262,61 @@ def test_float_num(self, xp): _ = xp.linspace(0, 1, num=2.5) +class TestLogspace: + @pytest.mark.parametrize("dtype", get_all_dtypes()) + @pytest.mark.parametrize("num", [2, 4, 8, 3, 9, 27]) + @pytest.mark.parametrize("endpoint", [True, False]) + def test_basic(self, dtype, num, endpoint): + start = 2 + stop = 5 + base = 2 + + func = lambda xp: xp.logspace( + start, stop, num, endpoint=endpoint, dtype=dtype, base=base + ) + + np_res = func(numpy) + dpnp_res = func(dpnp) + assert_allclose(dpnp_res, np_res, rtol=1e-06) + + @testing.with_requires("numpy>=1.25.0") + @pytest.mark.parametrize("axis", [0, 1]) + def test_axis(self, axis): + func = lambda xp: xp.logspace( + [2, 3], [20, 15], num=2, base=[[1, 3], [5, 7]], axis=axis + ) + assert_dtype_allclose(func(dpnp), func(numpy)) + + def test_list_input(self): + expected = numpy.logspace([0], [2], base=[5]) + result = dpnp.logspace([0], [2], base=[5]) + assert_dtype_allclose(result, expected) + + +class TestSpaceLike: + @pytest.mark.parametrize("func", ["geomspace", "linspace", "logspace"]) + @pytest.mark.parametrize( + "start_dtype", [numpy.float64, numpy.float32, numpy.int64, numpy.int32] + ) + @pytest.mark.parametrize( + "stop_dtype", [numpy.float64, numpy.float32, numpy.int64, numpy.int32] + ) + def test_numpy_dtype(self, func, start_dtype, stop_dtype): + start = numpy.array([1, 2, 3], dtype=start_dtype) + stop = numpy.array([11, 7, -2], dtype=stop_dtype) + getattr(dpnp, func)(start, stop, 10) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + @pytest.mark.parametrize("func", ["geomspace", "logspace"]) + @pytest.mark.parametrize( + "start, stop, num", + [(2, 5, -3), ([2, 3], 5, -3)], + ) + def test_space_num_error(self, xp, func, start, stop, num): + with pytest.raises(ValueError): + getattr(xp, func)(start, stop, num) + + class TestTrace: @pytest.mark.parametrize("a_sh", [(3, 4), (2, 2, 2)]) @pytest.mark.parametrize( @@ -871,19 +978,6 @@ def test_dpctl_tensor_input(func, args): assert_array_equal(X, Y) -@pytest.mark.parametrize("func", ["geomspace", "linspace", "logspace"]) -@pytest.mark.parametrize( - "start_dtype", [numpy.float64, numpy.float32, numpy.int64, numpy.int32] -) -@pytest.mark.parametrize( - "stop_dtype", [numpy.float64, numpy.float32, numpy.int64, numpy.int32] -) -def test_space_numpy_dtype(func, start_dtype, stop_dtype): - start = numpy.array([1, 2, 3], dtype=start_dtype) - stop = numpy.array([11, 7, -2], dtype=stop_dtype) - getattr(dpnp, func)(start, stop, 10) - - @pytest.mark.parametrize( "arrays", [[], [[1]], [[1, 2, 3], [4, 5, 6]], [[1, 2], [3, 4], [5, 6]]], @@ -908,104 +1002,6 @@ def test_set_shape(shape): assert_array_equal(na, da) -def test_geomspace_zero_error(): - with pytest.raises(ValueError): - dpnp.geomspace(0, 5, 3) - dpnp.geomspace(2, 0, 3) - dpnp.geomspace(0, 0, 3) - - -def test_space_num_error(): - with pytest.raises(ValueError): - dpnp.geomspace(2, 5, -3) - dpnp.logspace(2, 5, -3) - dpnp.geomspace([2, 3], 5, -3) - dpnp.logspace([2, 3], 5, -3) - - -@pytest.mark.parametrize("sign", [-1, 1]) -@pytest.mark.parametrize("dtype", get_all_dtypes()) -@pytest.mark.parametrize("num", [2, 4, 8, 3, 9, 27]) -@pytest.mark.parametrize("endpoint", [True, False]) -def test_geomspace(sign, dtype, num, endpoint): - start = 2 * sign - stop = 127 * sign - - func = lambda xp: xp.geomspace( - start, stop, num, endpoint=endpoint, dtype=dtype - ) - - np_res = func(numpy) - dpnp_res = func(dpnp) - - assert_allclose(dpnp_res, np_res, rtol=1e-06) - - -@pytest.mark.parametrize("start", [1j, 1 + 1j]) -@pytest.mark.parametrize("stop", [10j, 10 + 10j]) -def test_geomspace_complex(start, stop): - func = lambda xp: xp.geomspace(start, stop, num=10) - np_res = func(numpy) - dpnp_res = func(dpnp) - assert_allclose(dpnp_res, np_res, rtol=1e-06) - - -@pytest.mark.parametrize("axis", [0, 1]) -def test_geomspace_axis(axis): - func = lambda xp: xp.geomspace([2, 3], [20, 15], num=10, axis=axis) - np_res = func(numpy) - dpnp_res = func(dpnp) - assert_allclose(dpnp_res, np_res, rtol=1e-06) - - -def test_geomspace_num0(): - func = lambda xp: xp.geomspace(1, 10, num=0, endpoint=False) - np_res = func(numpy) - dpnp_res = func(dpnp) - assert_allclose(dpnp_res, np_res) - - -@pytest.mark.parametrize("dtype", get_all_dtypes()) -@pytest.mark.parametrize("num", [2, 4, 8, 3, 9, 27]) -@pytest.mark.parametrize("endpoint", [True, False]) -def test_logspace(dtype, num, endpoint): - if not is_win_platform() and is_tgllp_iris_xe() and is_lts_driver(): - if ( - dpnp.issubdtype(dtype, dpnp.integer) - and num in [8, 27] - and endpoint is True - ): - pytest.skip("SAT-7978") - - start = 2 - stop = 5 - base = 2 - - func = lambda xp: xp.logspace( - start, stop, num, endpoint=endpoint, dtype=dtype, base=base - ) - - np_res = func(numpy) - dpnp_res = func(dpnp) - - assert_allclose(dpnp_res, np_res, rtol=1e-06) - - -@testing.with_requires("numpy>=1.25.0") -@pytest.mark.parametrize("axis", [0, 1]) -def test_logspace_axis(axis): - func = lambda xp: xp.logspace( - [2, 3], [20, 15], num=2, base=[[1, 3], [5, 7]], axis=axis - ) - assert_dtype_allclose(func(dpnp), func(numpy)) - - -def test_logspace_list_input(): - expected = numpy.logspace([0], [2], base=[5]) - result = dpnp.logspace([0], [2], base=[5]) - assert_dtype_allclose(result, expected) - - @pytest.mark.parametrize( "data", [(), 1, (2, 3), [4], numpy.array(5), numpy.array([6, 7])] ) diff --git a/dpnp/tests/test_sycl_queue.py b/dpnp/tests/test_sycl_queue.py index 0bd4d6b5333..30256752ba0 100644 --- a/dpnp/tests/test_sycl_queue.py +++ b/dpnp/tests/test_sycl_queue.py @@ -133,10 +133,8 @@ def test_array_creation_from_array(func, args, device_x, device_y): assert_sycl_queue_equal(y.sycl_queue, x.sycl_queue) # cross device - # TODO: include geomspace when issue dpnp#2352 is resolved - if func != "geomspace": - y = getattr(dpnp, func)(*args, device=device_y) - assert_sycl_queue_equal(y.sycl_queue, x.to_device(device_y).sycl_queue) + y = getattr(dpnp, func)(*args, device=device_y) + assert_sycl_queue_equal(y.sycl_queue, x.to_device(device_y).sycl_queue) @pytest.mark.parametrize("device_x", valid_dev, ids=dev_ids) @@ -148,10 +146,9 @@ def test_array_creation_logspace_base(device_x, device_y): y = dpnp.logspace(0, 8, 4, base=x[1:3]) assert_sycl_queue_equal(y.sycl_queue, x.sycl_queue) - # TODO: include geomspace when issue dpnp#2353 is resolved # cross device - # y = dpnp.logspace(0, 8, 4, base=x[1:3], device=device_y) - # assert_sycl_queue_equal(y.sycl_queue, x.to_device(device_y).sycl_queue) + y = dpnp.logspace(0, 8, 4, base=x[1:3], device=device_y) + assert_sycl_queue_equal(y.sycl_queue, x.to_device(device_y).sycl_queue) @pytest.mark.parametrize("device", valid_dev + [None], ids=dev_ids + [None])