diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af5ac2d79..c09fea7af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Next Release] - TBD +### Added +- `dpctl.tensor.asarray`, `dpctl.tensor.empty` implemented (#646). + +### Changed - dpctl-capi is now renamed to `libsyclinterface` (#666). ## [0.11.1] - 11/10/2021 diff --git a/dpctl/tensor/__init__.py b/dpctl/tensor/__init__.py index 32e1e1ce46..6036099c62 100644 --- a/dpctl/tensor/__init__.py +++ b/dpctl/tensor/__init__.py @@ -33,13 +33,16 @@ from dpctl.tensor._copy_utils import copy_from_numpy as from_numpy from dpctl.tensor._copy_utils import copy_to_numpy as asnumpy from dpctl.tensor._copy_utils import copy_to_numpy as to_numpy +from dpctl.tensor._ctors import asarray, empty from dpctl.tensor._reshape import reshape from dpctl.tensor._usmarray import usm_ndarray __all__ = [ "usm_ndarray", + "asarray", "astype", "copy", + "empty", "reshape", "from_numpy", "to_numpy", diff --git a/dpctl/tensor/_copy_utils.py b/dpctl/tensor/_copy_utils.py index 7a7ba918f6..9422210a6d 100644 --- a/dpctl/tensor/_copy_utils.py +++ b/dpctl/tensor/_copy_utils.py @@ -93,12 +93,12 @@ def copy_to_numpy(ary): ) -def copy_from_numpy(np_ary, usm_type="device", queue=None): +def copy_from_numpy(np_ary, usm_type="device", sycl_queue=None): "Copies numpy array `np_ary` into a new usm_ndarray" # This may peform a copy to meet stated requirements Xnp = np.require(np_ary, requirements=["A", "O", "C", "E"]) - if queue: - ctor_kwargs = {"queue": queue} + if sycl_queue: + ctor_kwargs = {"queue": sycl_queue} else: ctor_kwargs = dict() Xusm = dpt.usm_ndarray( diff --git a/dpctl/tensor/_ctors.py b/dpctl/tensor/_ctors.py new file mode 100644 index 0000000000..d0f459d1c5 --- /dev/null +++ b/dpctl/tensor/_ctors.py @@ -0,0 +1,464 @@ +# Data Parallel Control (dpctl) +# +# Copyright 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +import dpctl +import dpctl.memory as dpm +import dpctl.tensor as dpt +import dpctl.utils + +_empty_tuple = tuple() +_host_set = frozenset([None]) + + +def _array_info_dispatch(obj): + if isinstance(obj, dpt.usm_ndarray): + return obj.shape, obj.dtype, frozenset([obj.sycl_queue]) + elif isinstance(obj, np.ndarray): + return obj.shape, obj.dtype, _host_set + elif isinstance(obj, range): + return (len(obj),), int, _host_set + elif isinstance(obj, float): + return _empty_tuple, float, _host_set + elif isinstance(obj, int): + return _empty_tuple, int, _host_set + elif isinstance(obj, complex): + return _empty_tuple, complex, _host_set + elif isinstance(obj, (list, tuple, range)): + return _array_info_sequence(obj) + else: + raise ValueError(type(obj)) + + +def _array_info_sequence(li): + assert isinstance(li, (list, tuple, range)) + n = len(li) + dim = None + dt = None + device = frozenset() + for el in li: + el_dim, el_dt, el_dev = _array_info_dispatch(el) + if dim is None: + dim = el_dim + dt = np.promote_types(el_dt, el_dt) + device = device.union(el_dev) + elif el_dim == dim: + dt = np.promote_types(dt, el_dt) + device = device.union(el_dev) + else: + raise ValueError( + "Inconsistent dimensions, {} and {}".format(dim, el_dim) + ) + return (n,) + dim, dt, device + + +def _normalize_queue_device(q=None, d=None): + if q is None: + d = dpt._device.Device.create_device(d) + return d.sycl_queue + else: + if not isinstance(q, dpctl.SyclQueue): + raise TypeError(f"Expected dpctl.SyclQueue, got {type(q)}") + if d is None: + return q + d = dpt._device.Device.create_device(d) + qq = dpctl.utils.get_execution_queue( + ( + q, + d.sycl_queue, + ) + ) + if qq is None: + raise TypeError( + "sycl_queue and device keywords can not be both specified" + ) + return qq + + +def _asarray_from_usm_ndarray( + usm_ndary, + dtype=None, + copy=None, + usm_type=None, + sycl_queue=None, + order="K", +): + if not isinstance(usm_ndary, dpt.usm_ndarray): + raise TypeError( + f"Expected dpctl.tensor.usm_ndarray, got {type(usm_ndary)}" + ) + if dtype is None: + dtype = usm_ndary.dtype + if usm_type is None: + usm_type = usm_ndary.usm_type + if sycl_queue is not None: + exec_q = dpctl.utils.get_execution_queue( + [usm_ndary.sycl_queue, sycl_queue] + ) + copy_q = _normalize_queue_device(q=sycl_queue, d=exec_q) + else: + copy_q = usm_ndary.sycl_queue + # Conditions for zero copy: + can_zero_copy = copy is not True + # dtype is unchanged + can_zero_copy = can_zero_copy and dtype == usm_ndary.dtype + # USM allocation type is unchanged + can_zero_copy = can_zero_copy and usm_type == usm_ndary.usm_type + # sycl_queue is unchanged + can_zero_copy = can_zero_copy and copy_q is usm_ndary.sycl_queue + # order is unchanged + c_contig = usm_ndary.flags & 1 + f_contig = usm_ndary.flags & 2 + fc_contig = usm_ndary.flags & 3 + if can_zero_copy: + if order == "C" and c_contig: + pass + elif order == "F" and f_contig: + pass + elif order == "A" and fc_contig: + pass + elif order == "K": + pass + else: + can_zero_copy = False + if copy is False and can_zero_copy is False: + raise ValueError("asarray(..., copy=False) is not possible") + if can_zero_copy: + return usm_ndary + if order == "A": + order = "F" if f_contig and not c_contig else "C" + if order == "K" and fc_contig: + order = "C" if c_contig else "F" + if order == "K": + # new USM allocation + res = dpt.usm_ndarray( + usm_ndary.shape, + dtype=dtype, + buffer=usm_type, + order="C", + buffer_ctor_kwargs={"queue": copy_q}, + ) + original_strides = usm_ndary.strides + ind = sorted( + range(usm_ndary.ndim), + key=lambda i: abs(original_strides[i]), + reverse=True, + ) + new_strides = tuple(res.strides[ind[i]] for i in ind) + # reuse previously made USM allocation + res = dpt.usm_ndarray( + usm_ndary.shape, + dtype=res.dtype, + buffer=res.usm_data, + strides=new_strides, + ) + else: + res = dpt.usm_ndarray( + usm_ndary.shape, + dtype=dtype, + buffer=usm_type, + order=order, + buffer_ctor_kwargs={"queue": copy_q}, + ) + # FIXME: call copy_to when implemented + res[(slice(None, None, None),) * res.ndim] = usm_ndary + return res + + +def _asarray_from_numpy_ndarray( + ary, dtype=None, usm_type=None, sycl_queue=None, order="K" +): + if not isinstance(ary, np.ndarray): + raise TypeError(f"Expected numpy.ndarray, got {type(ary)}") + if usm_type is None: + usm_type = "device" + if dtype is None: + dtype = ary.dtype + copy_q = _normalize_queue_device(q=None, d=sycl_queue) + f_contig = ary.flags["F"] + c_contig = ary.flags["C"] + fc_contig = f_contig or c_contig + if order == "A": + order = "F" if f_contig and not c_contig else "C" + if order == "K" and fc_contig: + order = "C" if c_contig else "F" + if order == "K": + # new USM allocation + res = dpt.usm_ndarray( + ary.shape, + dtype=dtype, + buffer=usm_type, + order="C", + buffer_ctor_kwargs={"queue": copy_q}, + ) + original_strides = ary.strides + ind = sorted( + range(ary.ndim), + key=lambda i: abs(original_strides[i]), + reverse=True, + ) + new_strides = tuple(res.strides[ind[i]] for i in ind) + # reuse previously made USM allocation + res = dpt.usm_ndarray( + res.shape, dtype=res.dtype, buffer=res.usm_data, strides=new_strides + ) + else: + res = dpt.usm_ndarray( + ary.shape, + dtype=dtype, + buffer=usm_type, + order=order, + buffer_ctor_kwargs={"queue": copy_q}, + ) + # FIXME: call copy_to when implemented + res[(slice(None, None, None),) * res.ndim] = ary + return res + + +def _is_object_with_buffer_protocol(obj): + "Returns True if object support Python buffer protocol" + try: + # use context manager to ensure + # buffer is instantly released + with memoryview(obj): + return True + except TypeError: + return False + + +def asarray( + obj, + dtype=None, + device=None, + copy=None, + usm_type=None, + sycl_queue=None, + order="K", +): + """asarray(obj, dtype=None, copy=None, order="K", + device=None, usm_type=None, sycl_queue=None) + + Converts `obj` to :class:`dpctl.tensor.usm_ndarray`. + + Args: + obj: Python object to convert. Can be an instance of `usm_ndarray`, + an object representing SYCL USM allocation and implementing + `__sycl_usm_array_interface__` protocol, an instance + of `numpy.ndarray`, an object supporting Python buffer protocol, + a Python scalar, or a (possibly nested) sequence of Python scalars. + dtype (data type, optional): output array data type. If `dtype` is + `None`, the output array data type is inferred from data types in + `obj`. Default: `None` + copy (`bool`, optional): boolean indicating whether or not to copy the + input. If `True`, always creates a copy. If `False`, need to copy + raises `ValueError`. If `None`, try to reuse existing memory + allocations if possible, but allowed to perform a copy otherwise. + Default: `None`. + order ("C","F","A","K", optional): memory layout of the output array. + Default: "C" + device (optional): array API concept of device where the output array + is created. `device` can be `None`, a oneAPI filter selector string, + an instance of :class:`dpctl.SyclDevice` corresponding to a + non-partitioned SYCL device, an instance of + :class:`dpctl.SyclQueue`, or a `Device` object returnedby + `dpctl.tensor.usm_array.device`. Default: `None`. + usm_type ("device"|"shared"|"host", optional): The type of SYCL USM + allocation for the output array. For `usm_type=None` the allocation + type is inferred from the input if `obj` has USM allocation, or + `"device"` is used instead. Default: `None`. + sycl_queue: (:class:`dpctl.SyclQueue`, optional): The SYCL queue to use + for output array allocation and copying. `sycl_queue` and `device` + are exclusive keywords, i.e. use one or another. If both are + specified, a `TypeError` is raised unless both imply the same + underlying SYCL queue to be used. If both a `None`, the + `dpctl.SyclQueue()` is used for allocation and copying. + Default: `None`. + """ + # 1. Check that copy is a valid keyword + if copy not in [None, True, False]: + raise TypeError( + "Recognized copy keyword values should be True, False, or None" + ) + # 2. Check that dtype is None, or a valid dtype + if dtype is not None: + dtype = np.dtype(dtype) + # 3. Validate order + if not isinstance(order, str): + raise TypeError( + f"Expected order keyword to be of type str, got {type(order)}" + ) + if len(order) == 0 or order[0] not in "KkAaCcFf": + raise ValueError( + "Unrecognized order keyword value, expecting 'K', 'A', 'F', or 'C'." + ) + else: + order = order[0].upper() + # 4. Check that usm_type is None, or a valid value + if usm_type is not None: + if isinstance(usm_type, str): + if usm_type not in ["device", "shared", "host"]: + raise ValueError( + f"Unrecognized value of usm_type={usm_type}, " + "expected 'device', 'shared', 'host', or None." + ) + else: + raise TypeError( + f"Expected usm_type to be a str or None, got {type(usm_type)}" + ) + # 5. Normalize device/sycl_queue [keep it None if was None] + if device is not None or sycl_queue is not None: + sycl_queue = _normalize_queue_device(q=sycl_queue, d=device) + + # handle instance(obj, usm_ndarray) + if isinstance(obj, dpt.usm_ndarray): + return _asarray_from_usm_ndarray( + obj, + dtype=dtype, + copy=copy, + usm_type=usm_type, + sycl_queue=sycl_queue, + order=order, + ) + elif hasattr(obj, "__sycl_usm_array_interface__"): + sua_iface = getattr(obj, "__sycl_usm_array_interface__") + membuf = dpm.as_usm_memory(obj) + ary = dpt.usm_ndarray( + sua_iface["shape"], + dtype=sua_iface["typestr"], + buffer=membuf, + strides=sua_iface.get("strides", None), + ) + return _asarray_from_usm_ndarray( + ary, + dtype=dtype, + copy=copy, + usm_type=usm_type, + sycl_queue=sycl_queue, + order=order, + ) + elif isinstance(obj, np.ndarray): + if copy is False: + raise ValueError( + "Converting numpy.ndarray to usm_ndarray requires a copy" + ) + return _asarray_from_numpy_ndarray( + obj, + dtype=dtype, + usm_type=usm_type, + sycl_queue=sycl_queue, + order=order, + ) + elif _is_object_with_buffer_protocol(obj): + if copy is False: + raise ValueError( + f"Converting {type(obj)} to usm_ndarray requires a copy" + ) + return _asarray_from_numpy_ndarray( + np.array(obj), + dtype=dtype, + usm_type=usm_type, + sycl_queue=sycl_queue, + order=order, + ) + elif isinstance(obj, (list, tuple, range)): + if copy is False: + raise ValueError( + "Converting Python sequence to usm_ndarray requires a copy" + ) + _, dt, devs = _array_info_sequence(obj) + if devs == _host_set: + return _asarray_from_numpy_ndarray( + np.asarray(obj, dt, order=order), + dtype=dtype, + usm_type=usm_type, + sycl_queue=sycl_queue, + order=order, + ) + # for sequences + raise NotImplementedError( + "Converting Python sequences is not implemented" + ) + if copy is False: + raise ValueError( + f"Converting {type(obj)} to usm_ndarray requires a copy" + ) + # obj is a scalar, create 0d array + return _asarray_from_numpy_ndarray( + np.asarray(obj), + dtype=dtype, + usm_type=usm_type, + sycl_queue=sycl_queue, + order="C", + ) + + +def empty( + sh, dtype="f8", order="C", device=None, usm_type="device", sycl_queue=None +): + """dpctl.tensor.empty(shape, dtype="f8", order="C", device=None, + usm_type="device", sycl_queue=None) + + Creates `usm_ndarray` from uninitialized USM allocation. + + Args: + shape (tuple): Dimensions of the array to be created. + dtype (optional): data type of the array. Can be typestring, + a `numpy.dtype` object, `numpy` char string, or a numpy + scalar type. Default: "f8" + order ("C", or F"): memory layout for the array. Default: "C" + device (optional): array API concept of device where the output array + is created. `device` can be `None`, a oneAPI filter selector string, + an instance of :class:`dpctl.SyclDevice` corresponding to a + non-partitioned SYCL device, an instance of + :class:`dpctl.SyclQueue`, or a `Device` object returnedby + `dpctl.tensor.usm_array.device`. Default: `None`. + usm_type ("device"|"shared"|"host", optional): The type of SYCL USM + allocation for the output array. Default: `"device"`. + sycl_queue: (:class:`dpctl.SyclQueue`, optional): The SYCL queue to use + for output array allocation and copying. `sycl_queue` and `device` + are exclusive keywords, i.e. use one or another. If both are + specified, a `TypeError` is raised unless both imply the same + underlying SYCL queue to be used. If both a `None`, the + `dpctl.SyclQueue()` is used for allocation and copying. + Default: `None`. + """ + dtype = np.dtype(dtype) + if not isinstance(order, str) or len(order) == 0 or order[0] not in "CcFf": + raise ValueError( + "Unrecognized order keyword value, expecting 'F' or 'C'." + ) + else: + order = order[0].upper() + if isinstance(usm_type, str): + if usm_type not in ["device", "shared", "host"]: + raise ValueError( + f"Unrecognized value of usm_type={usm_type}, " + "expected 'device', 'shared', or 'host'." + ) + else: + raise TypeError( + f"Expected usm_type to be of type str, got {type(usm_type)}" + ) + sycl_queue = _normalize_queue_device(q=sycl_queue, d=device) + res = dpt.usm_ndarray( + sh, + dtype=dtype, + buffer=usm_type, + order=order, + buffer_ctor_kwargs={"queue": sycl_queue}, + ) + return res diff --git a/dpctl/tests/test_tensor_asarray.py b/dpctl/tests/test_tensor_asarray.py new file mode 100644 index 0000000000..c7734b8194 --- /dev/null +++ b/dpctl/tests/test_tensor_asarray.py @@ -0,0 +1,192 @@ +# Data Parallel Control (dpctl) +# +# Copyright 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pytest + +import dpctl +import dpctl.tensor as dpt + + +@pytest.mark.parametrize( + "src_usm_type, dst_usm_type", + [ + ("device", "shared"), + ("device", "host"), + ("shared", "device"), + ("shared", "host"), + ("host", "device"), + ("host", "shared"), + ], +) +def test_asarray_change_usm_type(src_usm_type, dst_usm_type): + d = dpctl.SyclDevice() + if d.is_host: + pytest.skip( + "Skip test of host device, which only " + "supports host USM allocations" + ) + X = dpt.empty(10, dtype="u1", usm_type=src_usm_type) + Y = dpt.asarray(X, usm_type=dst_usm_type) + assert X.shape == Y.shape + assert X.usm_type == src_usm_type + assert Y.usm_type == dst_usm_type + + with pytest.raises(ValueError): + # zero copy is not possible + dpt.asarray(X, usm_type=dst_usm_type, copy=False) + + Y = dpt.asarray(X, usm_type=dst_usm_type, sycl_queue=X.sycl_queue) + assert X.shape == Y.shape + assert Y.usm_type == dst_usm_type + + Y = dpt.asarray( + X, + usm_type=dst_usm_type, + sycl_queue=X.sycl_queue, + device=d.get_filter_string(), + ) + assert X.shape == Y.shape + assert Y.usm_type == dst_usm_type + + +def test_asarray_from_numpy(): + Xnp = np.arange(10) + Y = dpt.asarray(Xnp, usm_type="device") + assert type(Y) is dpt.usm_ndarray + assert Y.shape == (10,) + assert Y.dtype == Xnp.dtype + + +def test_asarray_from_sequence(): + X = [1, 2, 3] + Y = dpt.asarray(X, usm_type="device") + assert type(Y) is dpt.usm_ndarray + + X = [(1, 1), (2.0, 2.0 + 1.0j), range(4, 6), np.array([3, 4], dtype="c16")] + Y = dpt.asarray(X, usm_type="device") + assert type(Y) is dpt.usm_ndarray + assert Y.ndim == 2 + + +def test_asarray_from_object_with_suai(): + """Test that asarray can deal with opaque objects implementing SUAI""" + + class Dummy: + def __init__(self, obj, iface): + self.obj = obj + self.__sycl_usm_array_interface__ = iface + + X = dpt.empty((2, 3, 4), dtype="f4") + Y = dpt.asarray(Dummy(X, X.__sycl_usm_array_interface__)) + assert Y.shape == X.shape + assert X.usm_type == Y.usm_type + assert X.dtype == Y.dtype + assert X.sycl_device == Y.sycl_device + + +def test_asarray_input_validation(): + with pytest.raises(TypeError): + # copy keyword is not of right type + dpt.asarray([1], copy="invalid") + with pytest.raises(TypeError): + # order keyword is not valid + dpt.asarray([1], order=1) + with pytest.raises(TypeError): + # dtype is not valid + dpt.asarray([1], dtype="invalid") + with pytest.raises(ValueError): + # unexpected value of order + dpt.asarray([1], order="Z") + with pytest.raises(TypeError): + # usm_type is of wrong type + dpt.asarray([1], usm_type=dict()) + with pytest.raises(ValueError): + # usm_type has wrong value + dpt.asarray([1], usm_type="mistake") + with pytest.raises(TypeError): + # sycl_queue type is not right + dpt.asarray([1], sycl_queue=dpctl.SyclContext()) + with pytest.raises(ValueError): + # sequence is not rectangular + dpt.asarray([[1], 2]) + + +def test_asarray_input_validation2(): + d = dpctl.get_devices() + if len(d) < 2: + pytest.skip("Not enough SYCL devices available") + + d0, d1 = d[:2] + try: + q0 = dpctl.SyclQueue(d0) + except dpctl.SyclQueueCreationError: + pytest.skip(f"SyclQueue could not be created for {d0}") + try: + q1 = dpctl.SyclQueue(d1) + except dpctl.SyclQueueCreationError: + pytest.skip(f"SyclQueue could not be created for {d1}") + with pytest.raises(TypeError): + dpt.asarray([1, 2], sycl_queue=q0, device=q1) + + +def test_asarray_scalars(): + import ctypes + + Y = dpt.asarray(5) + assert Y.dtype == np.dtype(int) + Y = dpt.asarray(5.2) + assert Y.dtype == np.dtype(float) + Y = dpt.asarray(np.float32(2.3)) + assert Y.dtype == np.dtype(np.float32) + Y = dpt.asarray(1.0j) + assert Y.dtype == np.dtype(complex) + Y = dpt.asarray(ctypes.c_int(8)) + assert Y.dtype == np.dtype(ctypes.c_int) + + +def test_asarray_copy_false(): + try: + q = dpctl.SyclQueue() + except dpctl.SyclQueueCreationError: + pytest.skip("Could not create a queue") + X = dpt.from_numpy(np.random.randn(10, 4), usm_type="device", sycl_queue=q) + Y1 = dpt.asarray(X, copy=False, order="K") + assert Y1 is X + Y1c = dpt.asarray(X, copy=True, order="K") + assert not (Y1c is X) + Y2 = dpt.asarray(X, copy=False, order="C") + assert Y2 is X + Y3 = dpt.asarray(X, copy=False, order="A") + assert Y3 is X + with pytest.raises(ValueError): + Y1 = dpt.asarray(X, copy=False, order="F") + Xf = dpt.empty( + X.shape, + dtype=X.dtype, + usm_type="device", + sycl_queue=X.sycl_queue, + order="F", + ) + Xf[:] = X + Y4 = dpt.asarray(Xf, copy=False, order="K") + assert Y4 is Xf + Y5 = dpt.asarray(Xf, copy=False, order="F") + assert Y5 is Xf + Y6 = dpt.asarray(Xf, copy=False, order="A") + assert Y6 is Xf + with pytest.raises(ValueError): + dpt.asarray(Xf, copy=False, order="C") diff --git a/dpctl/tests/test_usm_ndarray_ctor.py b/dpctl/tests/test_usm_ndarray_ctor.py index b5fab57566..7a33e8d87c 100644 --- a/dpctl/tests/test_usm_ndarray_ctor.py +++ b/dpctl/tests/test_usm_ndarray_ctor.py @@ -562,7 +562,7 @@ def test_pyx_capi_check_constants(): def test_tofrom_numpy(shape, dtype, usm_type): q = dpctl.SyclQueue() Xnp = np.zeros(shape, dtype=dtype) - Xusm = dpt.from_numpy(Xnp, usm_type=usm_type, queue=q) + Xusm = dpt.from_numpy(Xnp, usm_type=usm_type, sycl_queue=q) Ynp = np.ones(shape, dtype=dtype) ind = (slice(None, None, None),) * Ynp.ndim Xusm[ind] = Ynp