diff --git a/onedal/datatypes/_data_conversion.py b/onedal/datatypes/_data_conversion.py index 0caac10884..e52c4c7f47 100644 --- a/onedal/datatypes/_data_conversion.py +++ b/onedal/datatypes/_data_conversion.py @@ -22,40 +22,24 @@ from onedal import _backend, _is_dpc_backend from ..utils import _is_csr -from ..utils._dpep_helpers import is_dpctl_available -dpctl_available = is_dpctl_available("0.14") -if dpctl_available: - import dpctl - import dpctl.tensor as dpt - - -def _apply_and_pass(func, *args): +def _apply_and_pass(func, *args, **kwargs): if len(args) == 1: - return func(args[0]) - return tuple(map(func, args)) + return func(args[0], **kwargs) + return tuple(map(lambda arg: func(arg, **kwargs), args)) -def from_table(*args): - return _apply_and_pass(_backend.from_table, *args) +if _is_dpc_backend: + from ..utils._dpep_helpers import dpctl_available, dpnp_available -def convert_one_to_table(arg): if dpctl_available: - if isinstance(arg, dpt.usm_ndarray): - return _backend.dpctl_to_table(arg) - - if not _is_csr(arg): - arg = make2d(arg) - return _backend.to_table(arg) - + import dpctl.tensor as dpt -def to_table(*args): - return _apply_and_pass(convert_one_to_table, *args) + if dpnp_available: + import dpnp - -if _is_dpc_backend: from ..common._policy import _HostInteropPolicy def _convert_to_supported(policy, *data): @@ -85,6 +69,45 @@ def convert_or_pass(x): return _apply_and_pass(func, *data) + def convert_one_from_table(table, sycl_queue=None, sua_iface=None, xp=None): + # Currently only `__sycl_usm_array_interface__` protocol used to + # convert into dpnp/dpctl tensors. + if sua_iface: + if ( + sycl_queue + and sycl_queue.sycl_device.is_cpu + and table.__sycl_usm_array_interface__["syclobj"] is None + ): + # oneDAL returns tables with None sycl queue for CPU sycl queue inputs. + # This workaround is necessary for the functional preservation + # of the compute-follows-data execution. + # Host tables first converted into numpy.narrays and then to array from xp + # namespace. + return xp.asarray( + _backend.from_table(table), usm_type="device", sycl_queue=sycl_queue + ) + else: + xp_name = xp.__name__ + if dpnp_available and xp_name == "dpnp": + # By default DPNP ndarray created with a copy. + # TODO: + # investigate why dpnp.array(table, copy=False) doesn't work. + # Work around with using dpctl.tensor.asarray. + return dpnp.array(dpt.asarray(table), copy=False) + else: + return xp.asarray(table) + return _backend.from_table(table) + + def convert_one_to_table(arg, sua_iface=None): + # Note: currently only oneDAL homogen tables are supported and the + # contiuginity of the input array should be checked in advance. + if sua_iface: + return _backend.sua_iface_to_table(arg) + + if not _is_csr(arg): + arg = make2d(arg) + return _backend.to_table(arg) + else: def _convert_to_supported(policy, *data): @@ -92,3 +115,32 @@ def func(x): return x return _apply_and_pass(func, *data) + + def convert_one_from_table(table, sycl_queue=None, sua_iface=None, xp=None): + # Currently only `__sycl_usm_array_interface__` protocol used to + # convert into dpnp/dpctl tensors. + if sua_iface: + raise RuntimeError( + "SYCL usm array conversion from table requires the DPC backend" + ) + return _backend.from_table(table) + + def convert_one_to_table(arg, sua_iface=None): + if sua_iface: + raise RuntimeError( + "SYCL usm array conversion to table requires the DPC backend" + ) + + if not _is_csr(arg): + arg = make2d(arg) + return _backend.to_table(arg) + + +def from_table(*args, sycl_queue=None, sua_iface=None, xp=None): + return _apply_and_pass( + convert_one_from_table, *args, sycl_queue=sycl_queue, sua_iface=sua_iface, xp=xp + ) + + +def to_table(*args, sua_iface=None): + return _apply_and_pass(convert_one_to_table, *args, sua_iface=sua_iface) diff --git a/onedal/datatypes/data_conversion.cpp b/onedal/datatypes/data_conversion.cpp index 5e46810248..ad9832da8b 100644 --- a/onedal/datatypes/data_conversion.cpp +++ b/onedal/datatypes/data_conversion.cpp @@ -23,7 +23,7 @@ #include "oneapi/dal/table/detail/homogen_utils.hpp" #include "onedal/datatypes/data_conversion.hpp" -#include "onedal/datatypes/numpy_helpers.hpp" +#include "onedal/datatypes/utils/numpy_helpers.hpp" #include "onedal/version.hpp" #if ONEDAL_VERSION <= 20230100 diff --git a/onedal/datatypes/data_conversion_dpctl.cpp b/onedal/datatypes/data_conversion_dpctl.cpp deleted file mode 100644 index cbdf4725da..0000000000 --- a/onedal/datatypes/data_conversion_dpctl.cpp +++ /dev/null @@ -1,225 +0,0 @@ -/******************************************************************************* -* Copyright 2023 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. -*******************************************************************************/ - -#ifdef ONEDAL_DPCTL_INTEGRATION -#define NO_IMPORT_ARRAY - -#include -#include -#include - -#include "oneapi/dal/table/homogen.hpp" -#include "oneapi/dal/table/detail/homogen_utils.hpp" - -#include "onedal/datatypes/data_conversion_dpctl.hpp" -#include "onedal/datatypes/numpy_helpers.hpp" - -#include "dpctl4pybind11.hpp" - -namespace oneapi::dal::python { - -void report_problem_from_dptensor(const char* clarification) { - constexpr const char* const base_message = "Unable to convert from dptensor"; - - std::string message{ base_message }; - message += std::string{ clarification }; - throw std::invalid_argument{ message }; -} - -std::int64_t get_and_check_dptensor_ndim(const dpctl::tensor::usm_ndarray& tensor) { - constexpr const char* const err_message = ": only 1D & 2D tensors are allowed"; - - const auto ndim = dal::detail::integral_cast(tensor.get_ndim()); - if ((ndim != 1) && (ndim != 2)) - report_problem_from_dptensor(err_message); - return ndim; -} - -auto get_dptensor_shape(const dpctl::tensor::usm_ndarray& tensor) { - const auto ndim = get_and_check_dptensor_ndim(tensor); - std::int64_t row_count, col_count; - if (ndim == 1l) { - row_count = dal::detail::integral_cast(tensor.get_shape(0)); - col_count = 1l; - } - else { - row_count = dal::detail::integral_cast(tensor.get_shape(0)); - col_count = dal::detail::integral_cast(tensor.get_shape(1)); - } - - return std::make_pair(row_count, col_count); -} - -auto get_dptensor_layout(const dpctl::tensor::usm_ndarray& tensor) { - const auto ndim = get_and_check_dptensor_ndim(tensor); - const bool is_c_cont = tensor.is_c_contiguous(); - const bool is_f_cont = tensor.is_f_contiguous(); - - if (ndim == 1l) { - //if (!is_c_cont || !is_f_cont) report_problem_from_dptensor( - // ": 1D array should be contiguous both as C-order and F-order"); - return dal::data_layout::row_major; - } - else { - //if (!is_c_cont || !is_f_cont) report_problem_from_dptensor( - // ": 2D array should be contiguous at least by one axis"); - return is_c_cont ? dal::data_layout::row_major : dal::data_layout::column_major; - } -} - -template -dal::table convert_to_homogen_impl(py::object obj, dpctl::tensor::usm_ndarray& tensor) { - const dpctl::tensor::usm_ndarray* const ptr = &tensor; - const auto deleter = [obj](const Type*) { - obj.dec_ref(); - }; - const auto [r_count, c_count] = get_dptensor_shape(tensor); - const auto layout = get_dptensor_layout(tensor); - const auto* data = tensor.get_data(); - const auto queue = tensor.get_queue(); - - auto res = dal::homogen_table(queue, - data, - r_count, - c_count, // - deleter, - std::vector{}, - layout); - - obj.inc_ref(); - - return res; -} - -dal::table convert_from_dptensor(py::object obj) { - auto tensor = pybind11::cast(obj); - - const auto type = tensor.get_typenum(); - - dal::table res{}; - -#define MAKE_HOMOGEN_TABLE(CType) \ - res = convert_to_homogen_impl(obj, tensor); - - SET_NPY_FEATURE(type, - MAKE_HOMOGEN_TABLE, // - report_problem_from_dptensor(": unknown data type")); - -#undef MAKE_HOMOGEN_TABLE - - return res; -} - -void report_problem_to_dptensor(const char* clarification) { - constexpr const char* const base_message = "Unable to convert to dptensor"; - - std::string message{ base_message }; - message += std::string{ clarification }; - throw std::runtime_error{ message }; -} - -// TODO: -// return type. -std::string get_npy_typestr(const dal::data_type dtype) { - switch (dtype) { - case dal::data_type::float32: { - return "(input); - const dal::data_type dtype = homogen_input.get_metadata().get_data_type(0); - const dal::data_layout data_layout = homogen_input.get_data_layout(); - - npy_intp row_count = dal::detail::integral_cast( - homogen_input.get_row_count()); - npy_intp column_count = dal::detail::integral_cast( - homogen_input.get_column_count()); - - // need "version", "data", "shape", "typestr", "syclobj" - py::tuple shape = py::make_tuple(row_count, column_count); - py::list data_entry(2); - - auto bytes_array = dal::detail::get_original_data(homogen_input); - if (!bytes_array.get_queue().has_value()) { - report_problem_to_dptensor(": table has no queue"); - } - auto queue = bytes_array.get_queue().value(); - - const bool is_mutable = bytes_array.has_mutable_data(); - - static_assert(sizeof(std::size_t) == sizeof(void*)); - data_entry[0] = is_mutable ? reinterpret_cast(bytes_array.get_mutable_data()) - : reinterpret_cast(bytes_array.get_data()); - data_entry[1] = is_mutable; - - py::dict iface; - iface["data"] = data_entry; - iface["shape"] = shape; - iface["strides"] = get_npy_strides(data_layout, row_count, column_count); - // dpctl supports only version 1. - iface["version"] = 1; - iface["typestr"] = get_npy_typestr(dtype); - iface["syclobj"] = py::cast(queue); - - return iface; -} - -// We are using `__sycl_usm_array_interface__` attribute for constructing -// dpctl tensor on python level. -void define_sycl_usm_array_property(py::class_& table_obj) { - table_obj.def_property_readonly("__sycl_usm_array_interface__", &construct_sua_iface); -} - -} // namespace oneapi::dal::python - -#endif // ONEDAL_DPCTL_INTEGRATION diff --git a/onedal/datatypes/data_conversion_sua_iface.cpp b/onedal/datatypes/data_conversion_sua_iface.cpp new file mode 100644 index 0000000000..4a83ad6fdc --- /dev/null +++ b/onedal/datatypes/data_conversion_sua_iface.cpp @@ -0,0 +1,221 @@ +/******************************************************************************* +* Copyright 2024 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. +*******************************************************************************/ + +#ifdef ONEDAL_DATA_PARALLEL +#define NO_IMPORT_ARRAY + +#include +#include +#include + +#include "oneapi/dal/common.hpp" +#include "oneapi/dal/detail/common.hpp" +#include "oneapi/dal/table/homogen.hpp" +#include "oneapi/dal/table/detail/homogen_utils.hpp" + +#include "onedal/common/policy_common.hpp" +#include "onedal/datatypes/data_conversion_sua_iface.hpp" +#include "onedal/datatypes/utils/dtype_conversions.hpp" +#include "onedal/datatypes/utils/dtype_dispatcher.hpp" +#include "onedal/datatypes/utils/sua_iface_helpers.hpp" + +namespace oneapi::dal::python { + +// Please follow +// for the description of `__sycl_usm_array_interface__` protocol. + +// Convert python object to oneDAL homogen table with zero-copy by use +// of `__sycl_usm_array_interface__` protocol. +template +dal::table convert_to_homogen_impl(py::object obj) { + // Get `__sycl_usm_array_interface__` dictionary representing USM allocations. + auto sua_iface_dict = get_sua_interface(obj); + + // Python uses reference counting as its primary memory management technique. + // Each object in Python has an associated reference count, representing the number + // of references pointing to that object. When this count drops to zero, Python + // automatically frees the memory occupied by the object. + // Using as a deleter the convenience function for decreasing the reference count + // of an instance and potentially deleting it when the count reaches zero. + const auto deleter = [obj](const Type*) { + obj.dec_ref(); + }; + + // Get and check `__sycl_usm_array_interface__` number of dimensions. + const auto ndim = get_and_check_sua_iface_ndim(sua_iface_dict); + + // Get the pair of row and column counts. + const auto [r_count, c_count] = get_sua_iface_shape_by_values(sua_iface_dict, ndim); + + // Get oneDAL Homogen DataLayout enumeration from input object shape and strides. + const auto layout = get_sua_iface_layout(sua_iface_dict, r_count, c_count); + + // Get `__sycl_usm_array_interface__['data'][0]`, the first element of data entry, + // which is a Python integer encoding USM pointer value. + const auto* const ptr = reinterpret_cast(get_sua_ptr(sua_iface_dict)); + + // Get SYCL object from `__sycl_usm_array_interface__["syclobj"]`. + // syclobj: Python object from which SYCL context to which represented USM + // allocation is bound. + auto syclobj = sua_iface_dict["syclobj"].cast(); + + // Get sycl::queue from syclobj. + const auto queue = get_queue_from_python(syclobj); + + // Use read-only accessor for onedal table. + bool is_readonly = is_sua_readonly(sua_iface_dict); + + dal::table res{}; + + if (is_readonly) { + res = dal::homogen_table(queue, + ptr, + r_count, + c_count, + deleter, + std::vector{}, + layout); + } + else { + auto* const mut_ptr = const_cast(ptr); + res = dal::homogen_table(queue, + mut_ptr, + r_count, + c_count, + deleter, + std::vector{}, + layout); + } + + // Towards the python object memory model increment the python object reference + // count due to new reference by oneDAL table pointing to that object. + obj.inc_ref(); + return res; +} + +// Convert oneDAL table with zero-copy by use of `__sycl_usm_array_interface__` protocol. +dal::table convert_from_sua_iface(py::object obj) { + // Get `__sycl_usm_array_interface__` dictionary representing USM allocations. + auto sua_iface_dict = get_sua_interface(obj); + + // Convert a string encoding elemental data type of the array to oneDAL homogen table + // data type. + const auto type = get_sua_dtype(sua_iface_dict); + + dal::table res{}; + +#define MAKE_HOMOGEN_TABLE(CType) res = convert_to_homogen_impl(obj); + + SET_CTYPE_FROM_DAL_TYPE(type, + MAKE_HOMOGEN_TABLE, + report_problem_for_sua_iface(": unknown data type")); + +#undef MAKE_HOMOGEN_TABLE + + return res; +} + +// Create a dictionary for `__sycl_usm_array_interface__` protocol from oneDAL table properties. +py::dict construct_sua_iface(const dal::table& input) { + const auto kind = input.get_kind(); + if (kind != dal::homogen_table::kind()) + report_problem_to_sua_iface(": only homogen tables are supported"); + + const auto& homogen_input = reinterpret_cast(input); + const dal::data_type dtype = homogen_input.get_metadata().get_data_type(0); + const dal::data_layout data_layout = homogen_input.get_data_layout(); + + npy_intp row_count = dal::detail::integral_cast(homogen_input.get_row_count()); + npy_intp column_count = dal::detail::integral_cast(homogen_input.get_column_count()); + + // `__sycl_usm_array_interface__` protocol is a Python dictionary with the following fields: + // shape: tuple of int + // typestr: string + // typedescr: a list of tuples + // data: (int, bool) + // strides: tuple of int + // offset: int + // version: int + // syclobj: dpctl.SyclQueue or dpctl.SyclContext object or `SyclQueueRef` PyCapsule that + // represents an opaque value of sycl::queue. + + py::tuple shape = py::make_tuple(row_count, column_count); + py::list data_entry(2); + + auto bytes_array = dal::detail::get_original_data(homogen_input); + auto has_queue = bytes_array.get_queue().has_value(); + // oneDAL returns tables without sycl context for CPU sycl queue inputs, that + // breaks the compute-follows-data execution. + // Currently not throwing runtime exception and __sycl_usm_array_interface__["syclobj"] None asigned + // if no SYCL queue to allow workaround on python side. + // if (!has_queue) { + // report_problem_to_sua_iface(": table has no queue"); + // } + + const bool is_mutable = bytes_array.has_mutable_data(); + + // data: A 2-tuple whose first element is a Python integer encoding + // USM pointer value. The second entry in the tuple is a read-only flag + // (True means the data area is read-only). + data_entry[0] = is_mutable ? reinterpret_cast(bytes_array.get_mutable_data()) + : reinterpret_cast(bytes_array.get_data()); + data_entry[1] = is_mutable; + + py::dict iface; + iface["data"] = data_entry; + // shape: a tuple of integers describing dimensions of an N-dimensional array. + // Note: oneDAL supports only (r,1) for 1-D arrays. In python code after from_table conversion + // for 1-D expected outputs xp.ravel or reshape(-1) is used. + // TODO: + // probably worth to update for 1-D arrays. + iface["shape"] = shape; + + // strides: An optional tuple of integers describing number of array elements needed to jump + // to the next array element in the corresponding dimensions. + iface["strides"] = get_npy_strides(data_layout, row_count, column_count); + + // Version of the `__sycl_usm_array_interface__`. At present, the only supported value is 1. + iface["version"] = 1; + + // Convert oneDAL homogen table data type to a string encoding elemental data type of the array. + iface["typestr"] = convert_dal_to_sua_type(dtype); + + // syclobj: Python object from which SYCL context to which represented USM allocation is bound. + if (!has_queue) { + iface["syclobj"] = py::none(); + } + else { + iface["syclobj"] = + pack_queue(std::make_shared(bytes_array.get_queue().value())); + } + + return iface; +} + +// Adding `__sycl_usm_array_interface__` attribute to python oneDAL table, that representing +// USM allocations. +void define_sycl_usm_array_property(py::class_& table_obj) { + // To enable native extensions to pass the memory allocated by a native SYCL library to SYCL-aware + // Python extension without making a copy, the class must provide `__sycl_usm_array_interface__` + // attribute representing USM allocations. The `__sycl_usm_array_interface__` attribute is used + // for constructing DPCTL usm_ndarray or DPNP ndarray with zero-copy on python level. + table_obj.def_property_readonly("__sycl_usm_array_interface__", &construct_sua_iface); +} + +} // namespace oneapi::dal::python + +#endif // ONEDAL_DATA_PARALLEL diff --git a/onedal/datatypes/data_conversion_dpctl.hpp b/onedal/datatypes/data_conversion_sua_iface.hpp similarity index 73% rename from onedal/datatypes/data_conversion_dpctl.hpp rename to onedal/datatypes/data_conversion_sua_iface.hpp index b9fdb64b16..4d4b33e4da 100644 --- a/onedal/datatypes/data_conversion_dpctl.hpp +++ b/onedal/datatypes/data_conversion_sua_iface.hpp @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright 2023 Intel Corporation +* Copyright 2024 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,9 +27,14 @@ namespace oneapi::dal::python { namespace py = pybind11; -dal::table convert_from_dptensor(py::object obj); +// Convert oneDAL table with zero-copy by use of `__sycl_usm_array_interface__` protocol. +dal::table convert_from_sua_iface(py::object obj); + +// Create a dictionary for `__sycl_usm_array_interface__` protocol from oneDAL table properties. py::dict construct_sua_iface(const dal::table& input); +// Adding `__sycl_usm_array_interface__` attribute to python oneDAL table, that representing +// USM allocations. void define_sycl_usm_array_property(py::class_& t); } // namespace oneapi::dal::python diff --git a/onedal/datatypes/table.cpp b/onedal/datatypes/table.cpp index c4a14a9d3f..9771306118 100644 --- a/onedal/datatypes/table.cpp +++ b/onedal/datatypes/table.cpp @@ -17,12 +17,12 @@ #include "oneapi/dal/table/common.hpp" #include "oneapi/dal/table/homogen.hpp" -#ifdef ONEDAL_DPCTL_INTEGRATION -#include "onedal/datatypes/data_conversion_dpctl.hpp" -#endif // ONEDAL_DPCTL_INTEGRATION +#ifdef ONEDAL_DATA_PARALLEL +#include "onedal/datatypes/data_conversion_sua_iface.hpp" +#endif // ONEDAL_DATA_PARALLEL #include "onedal/datatypes/data_conversion.hpp" -#include "onedal/datatypes/numpy_helpers.hpp" +#include "onedal/datatypes/utils/numpy_helpers.hpp" #include "onedal/common/pybind11_helpers.hpp" #include "onedal/version.hpp" @@ -73,9 +73,9 @@ ONEDAL_PY_INIT_MODULE(table) { return py::make_tuple(row_count, column_count); }); -#ifdef ONEDAL_DPCTL_INTEGRATION +#ifdef ONEDAL_DATA_PARALLEL define_sycl_usm_array_property(table_obj); -#endif // ONEDAL_DPCTL_INTEGRATION +#endif // ONEDAL_DATA_PARALLEL m.def("to_table", [](py::object obj) { auto* obj_ptr = obj.ptr(); @@ -87,11 +87,11 @@ ONEDAL_PY_INIT_MODULE(table) { return obj_ptr; }); -#ifdef ONEDAL_DPCTL_INTEGRATION - m.def("dpctl_to_table", [](py::object obj) { - return convert_from_dptensor(obj); +#ifdef ONEDAL_DATA_PARALLEL + m.def("sua_iface_to_table", [](py::object obj) { + return convert_from_sua_iface(obj); }); -#endif // ONEDAL_DPCTL_INTEGRATION +#endif // ONEDAL_DATA_PARALLEL } } // namespace oneapi::dal::python diff --git a/onedal/datatypes/table_metadata.cpp b/onedal/datatypes/table_metadata.cpp index 3b265664d3..2ddd32570c 100644 --- a/onedal/datatypes/table_metadata.cpp +++ b/onedal/datatypes/table_metadata.cpp @@ -16,7 +16,7 @@ #include "oneapi/dal/table/common.hpp" -#include "onedal/datatypes/numpy_helpers.hpp" +#include "onedal/datatypes/utils/numpy_helpers.hpp" #include "onedal/common/pybind11_helpers.hpp" #include "onedal/version.hpp" diff --git a/onedal/datatypes/tests/common.py b/onedal/datatypes/tests/common.py new file mode 100644 index 0000000000..37d68909a0 --- /dev/null +++ b/onedal/datatypes/tests/common.py @@ -0,0 +1,126 @@ +# =============================================================================== +# Copyright 2024 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. +# =============================================================================== + +from onedal.utils._dpep_helpers import dpctl_available, dpnp_available + +if dpnp_available: + import dpnp + +if dpctl_available: + import dpctl + from dpctl.tensor import usm_ndarray + + def _get_sycl_queue(syclobj): + if hasattr(syclobj, "_get_capsule"): + return dpctl.SyclQueue(syclobj._get_capsule()) + else: + return dpctl.SyclQueue(syclobj) + + def _assert_tensor_attr(actual, desired, order): + """Check attributes of two given USM tensors.""" + is_usm_tensor = ( + lambda x: dpnp_available + and isinstance(x, dpnp.ndarray) + or isinstance(x, usm_ndarray) + ) + assert is_usm_tensor(actual) + assert is_usm_tensor(desired) + # dpctl.tensor is the dpnp.ndarrays's core tensor structure along + # with advanced device management. Convert dpnp to dpctl.tensor with zero copy. + get_tensor = lambda x: ( + x.get_array() if dpnp_available and isinstance(x, dpnp.ndarray) else x + ) + # Now DPCtl tensors + actual = get_tensor(actual) + desired = get_tensor(desired) + + assert actual.shape == desired.shape + assert actual.strides == desired.strides + assert actual.dtype == desired.dtype + if order == "F": + assert actual.flags.f_contiguous + assert desired.flags.f_contiguous + assert actual.flags.f_contiguous == desired.flags.f_contiguous + else: + assert actual.flags.c_contiguous + assert desired.flags.c_contiguous + assert actual.flags.c_contiguous == desired.flags.c_contiguous + assert actual.flags == desired.flags + assert actual.sycl_queue == desired.sycl_queue + # TODO: + # check better way to check usm ptrs. + assert actual.usm_data._pointer == desired.usm_data._pointer + + def _assert_sua_iface_fields( + actual, desired, skip_syclobj=False, skip_data_0=False, skip_data_1=False + ): + """Check attributes of two given reprsesentations of + USM allocations `__sycl_usm_array_interface__`. + + For full documentation about `__sycl_usm_array_interface__` refer + https://intelpython.github.io/dpctl/latest/api_reference/dpctl/sycl_usm_array_interface.html. + + Parameters + ---------- + actual : dict, __sycl_usm_array_interface__ + desired : dict, __sycl_usm_array_interface__ + skip_syclobj : bool, default=False + If True, check for __sycl_usm_array_interface__["syclobj"] + will be skipped. + skip_data_0 : bool, default=False + If True, check for __sycl_usm_array_interface__["data"][0] + will be skipped. + skip_data_1 : bool, default=False + If True, check for __sycl_usm_array_interface__["data"][1] + will be skipped. + """ + assert hasattr(actual, "__sycl_usm_array_interface__") + assert hasattr(desired, "__sycl_usm_array_interface__") + actual_sua_iface = actual.__sycl_usm_array_interface__ + desired_sua_iface = desired.__sycl_usm_array_interface__ + # data: A 2-tuple whose first element is a Python integer encoding + # USM pointer value. The second entry in the tuple is a read-only flag + # (True means the data area is read-only). + if not skip_data_0: + assert actual_sua_iface["data"][0] == desired_sua_iface["data"][0] + if not skip_data_1: + assert actual_sua_iface["data"][1] == desired_sua_iface["data"][1] + # shape: a tuple of integers describing dimensions of an N-dimensional array. + # Reformating shapes for check cases (r,) vs (r,1). Contiguous flattened array + # shape (r,) becoming (r,1) just for the check, since oneDAL supports only (r,1) + # for 1-D arrays. In code after from_table conversion for 1-D expected outputs + # xp.ravel or reshape(-1) is used. + get_shape_if_1d = lambda shape: (shape[0], 1) if len(shape) == 1 else shape + actual_shape = get_shape_if_1d(actual_sua_iface["shape"]) + desired_shape = get_shape_if_1d(desired_sua_iface["shape"]) + assert actual_shape == desired_shape + # strides: An optional tuple of integers describing number of array elements + # needed to jump to the next array element in the corresponding dimensions. + if not actual_sua_iface["strides"] and not desired_sua_iface["strides"]: + # None to indicate a C-style contiguous 1D array. + # onedal4py constructs __sycl_usm_array_interface__["strides"] with + # real values. + assert actual_sua_iface["strides"] == desired_sua_iface["strides"] + # versions: Version of the interface. + assert actual_sua_iface["version"] == desired_sua_iface["version"] + # typestr: a string encoding elemental data type of the array. + assert actual_sua_iface["typestr"] == desired_sua_iface["typestr"] + # syclobj: Python object from which SYCL context to which represented USM + # allocation is bound. + if not skip_syclobj and dpctl_available: + actual_sycl_queue = _get_sycl_queue(actual_sua_iface["syclobj"]) + desired_sycl_queue = _get_sycl_queue(desired_sua_iface["syclobj"]) + assert actual_sycl_queue == desired_sycl_queue diff --git a/onedal/datatypes/tests/test_data.py b/onedal/datatypes/tests/test_data.py index a94efc8eaa..471d6f0a64 100644 --- a/onedal/datatypes/tests/test_data.py +++ b/onedal/datatypes/tests/test_data.py @@ -18,17 +18,76 @@ import pytest from numpy.testing import assert_allclose -from onedal import _backend +from onedal import _backend, _is_dpc_backend from onedal.datatypes import from_table, to_table -from onedal.primitives import linear_kernel -from onedal.tests.utils._device_selection import get_queues -from onedal.utils._dpep_helpers import is_dpctl_available - -dpctl_available = is_dpctl_available("0.14") +from onedal.utils._dpep_helpers import dpctl_available if dpctl_available: - import dpctl - import dpctl.tensor as dpt + from onedal.datatypes.tests.common import ( + _assert_sua_iface_fields, + _assert_tensor_attr, + ) + +from onedal.primitives import linear_kernel +from onedal.tests.utils._dataframes_support import ( + _convert_to_dataframe, + get_dataframes_and_queues, +) +from onedal.tests.utils._device_selection import get_queues +from onedal.utils._array_api import _get_sycl_namespace + +data_shapes = [ + pytest.param((1000, 100), id="(1000, 100)"), # 2-D array + pytest.param((2000, 50), id="(2000, 50)"), # 2-D array + pytest.param((50, 1), id="(50, 1)"), # 2-D array + pytest.param((1, 50), id="(1, 50)"), # 2-D array + pytest.param((50,), id="(50,)"), # 1-D array +] + +unsupported_data_shapes = [ + pytest.param((2, 3, 4), id="(2, 3, 4)"), + pytest.param((2, 3, 4, 5), id="(2, 3, 4, 5)"), +] + +ORDER_DICT = {"F": np.asfortranarray, "C": np.ascontiguousarray} + + +if _is_dpc_backend: + from daal4py.sklearn._utils import get_dtype + from onedal.cluster.dbscan import BaseDBSCAN + from onedal.common._policy import _get_policy + + class DummyEstimatorWithTableConversions: + + def fit(self, X, y=None): + sua_iface, xp, _ = _get_sycl_namespace(X) + policy = _get_policy(X.sycl_queue, None) + bs_DBSCAN = BaseDBSCAN() + types = [xp.float32, xp.float64] + if get_dtype(X) not in types: + X = xp.astype(X, dtype=xp.float64) + dtype = get_dtype(X) + params = bs_DBSCAN._get_onedal_params(dtype) + X_table = to_table(X, sua_iface=sua_iface) + # TODO: + # check other candidates for the dummy base oneDAL func. + # oneDAL backend func is needed to check result table checks. + result = _backend.dbscan.clustering.compute( + policy, params, X_table, to_table(None) + ) + result_responses_table = result.responses + result_responses_df = from_table( + result_responses_table, + sua_iface=sua_iface, + sycl_queue=X.sycl_queue, + xp=xp, + ) + return X_table, result_responses_table, result_responses_df + +else: + + class DummyEstimatorWithTableConversions: + pass def _test_input_format_c_contiguous_numpy(queue, dtype): @@ -166,69 +225,190 @@ def test_conversion_to_table(dtype): _test_conversion_to_table(dtype) -# TODO: -# Currently `dpctl_to_table` is not used in onedal estimators. -# The test will be enabled after future data management update, that brings -# re-impl of conversions between onedal tables and usm ndarrays. -@pytest.mark.skip( - reason="Currently removed. Will be enabled after data management update" +@pytest.mark.skipif( + not dpctl_available, + reason="dpctl is required for checks.", +) +@pytest.mark.skipif( + not _is_dpc_backend, + reason="__sycl_usm_array_interface__ support requires DPC backend.", ) -@pytest.mark.skipif(not dpctl_available, reason="requires dpctl>=0.14") -@pytest.mark.parametrize("queue", get_queues("cpu,gpu")) +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("dpctl,dpnp", "cpu,gpu") +) +@pytest.mark.parametrize("order", ["C", "F"]) @pytest.mark.parametrize("dtype", [np.float32, np.float64, np.int32, np.int64]) -def test_input_format_c_contiguous_dpctl(queue, dtype): +def test_input_sua_iface_zero_copy(dataframe, queue, order, dtype): + """Checking that values ​​representing USM allocations `__sycl_usm_array_interface__` + are preserved during conversion to onedal table. + """ rng = np.random.RandomState(0) - x_default = np.array(5 * rng.random_sample((10, 59)), dtype=dtype) + X_np = np.array(5 * rng.random_sample((10, 59)), dtype=dtype) - x_numpy = np.asanyarray(x_default, dtype=dtype, order="C") - x_dpt = dpt.asarray(x_numpy, usm_type="device", sycl_queue=queue) - # assert not x_dpt.flags.fnc - assert isinstance(x_dpt, dpt.usm_ndarray) + X_np = np.asanyarray(X_np, dtype=dtype, order=order) - x_table = _backend.dpctl_to_table(x_dpt) - assert hasattr(x_table, "__sycl_usm_array_interface__") - x_dpt_from_table = dpt.asarray(x_table) + X_dp = _convert_to_dataframe(X_np, sycl_queue=queue, target_df=dataframe) - assert ( - x_dpt.__sycl_usm_array_interface__["data"][0] - == x_dpt_from_table.__sycl_usm_array_interface__["data"][0] + sua_iface, X_dp_namespace, _ = _get_sycl_namespace(X_dp) + + X_table = to_table(X_dp, sua_iface=sua_iface) + _assert_sua_iface_fields(X_dp, X_table) + + X_dp_from_table = from_table( + X_table, sycl_queue=queue, sua_iface=sua_iface, xp=X_dp_namespace ) - assert x_dpt.shape == x_dpt_from_table.shape - assert x_dpt.strides == x_dpt_from_table.strides - assert x_dpt.dtype == x_dpt_from_table.dtype - assert x_dpt.flags.c_contiguous - assert x_dpt_from_table.flags.c_contiguous - - -# TODO: -# Currently `dpctl_to_table` is not used in onedal estimators. -# The test will be enabled after future data management update, that brings -# re-impl of conversions between onedal tables and usm ndarrays. -@pytest.mark.skip( - reason="Currently removed. Will be enabled after data management update" + _assert_sua_iface_fields(X_table, X_dp_from_table) + _assert_tensor_attr(X_dp, X_dp_from_table, order) + + +@pytest.mark.skipif( + not dpctl_available, + reason="dpctl is required for checks.", ) -@pytest.mark.skipif(not dpctl_available, reason="requires dpctl>=0.14") -@pytest.mark.parametrize("queue", get_queues("cpu,gpu")) -@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.int32, np.int64]) -def test_input_format_f_contiguous_dpctl(queue, dtype): +@pytest.mark.skipif( + not _is_dpc_backend, + reason="__sycl_usm_array_interface__ support requires DPC backend.", +) +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("dpctl,dpnp", "cpu,gpu") +) +@pytest.mark.parametrize("order", ["F", "C"]) +@pytest.mark.parametrize("data_shape", data_shapes) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_table_conversions(dataframe, queue, order, data_shape, dtype): + """Checking that values ​​representing USM allocations `__sycl_usm_array_interface__` + are preserved during conversion to onedal table and from onedal table to + sycl usm array dataformat. + """ rng = np.random.RandomState(0) - x_default = np.array(5 * rng.random_sample((10, 59)), dtype=dtype) - - x_numpy = np.asanyarray(x_default, dtype=dtype, order="F") - x_dpt = dpt.asarray(x_numpy, usm_type="device", sycl_queue=queue) - # assert not x_dpt.flags.fnc - assert isinstance(x_dpt, dpt.usm_ndarray) + X = np.array(5 * rng.random_sample(data_shape), dtype=dtype) + + X = ORDER_DICT[order](X) + + X = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) + alg = DummyEstimatorWithTableConversions() + X_table, result_responses_table, result_responses_df = alg.fit(X) + + for obj in [X_table, result_responses_table, result_responses_df, X]: + assert hasattr(obj, "__sycl_usm_array_interface__"), f"{obj} has no SUA interface" + _assert_sua_iface_fields(X, X_table) + + # Work around for saving compute-follows-data execution + # for CPU sycl context requires cause additional memory + # allocation using the same queue. + skip_data_0 = True if queue.sycl_device.is_cpu else False + # Onedal return table's syclobj is empty for CPU inputs. + skip_syclobj = True if queue.sycl_device.is_cpu else False + # TODO: + # investigate why __sycl_usm_array_interface__["data"][1] is changed + # after conversion from onedal table to sua array. + # Test is not turned off because of this. Only check is skipped. + skip_data_1 = True + _assert_sua_iface_fields( + result_responses_df, + result_responses_table, + skip_data_0=skip_data_0, + skip_data_1=skip_data_1, + skip_syclobj=skip_syclobj, + ) + assert X.sycl_queue == result_responses_df.sycl_queue + if order == "F": + assert X.flags.f_contiguous == result_responses_df.flags.f_contiguous + else: + assert X.flags.c_contiguous == result_responses_df.flags.c_contiguous + # 1D output expected to have the same c_contiguous and f_contiguous flag values. + assert ( + result_responses_df.flags.c_contiguous == result_responses_df.flags.f_contiguous + ) - x_table = _backend.dpctl_to_table(x_dpt) - assert hasattr(x_table, "__sycl_usm_array_interface__") - x_dpt_from_table = dpt.asarray(x_table) - assert ( - x_dpt.__sycl_usm_array_interface__["data"][0] - == x_dpt_from_table.__sycl_usm_array_interface__["data"][0] +@pytest.mark.skipif( + not _is_dpc_backend, + reason="__sycl_usm_array_interface__ support requires DPC backend.", +) +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("dpctl,dpnp", "cpu,gpu") +) +@pytest.mark.parametrize("data_shape", unsupported_data_shapes) +def test_sua_iface_interop_invalid_shape(dataframe, queue, data_shape): + X = np.zeros(data_shape) + X = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) + sua_iface, _, _ = _get_sycl_namespace(X) + + expected_err_msg = ( + "Unable to convert from SUA interface: only 1D & 2D tensors are allowed" ) - assert x_dpt.shape == x_dpt_from_table.shape - assert x_dpt.strides == x_dpt_from_table.strides - assert x_dpt.dtype == x_dpt_from_table.dtype - assert x_dpt.flags.f_contiguous - assert x_dpt_from_table.flags.f_contiguous + with pytest.raises(ValueError, match=expected_err_msg): + to_table(X, sua_iface=sua_iface) + + +@pytest.mark.skipif( + not _is_dpc_backend, + reason="__sycl_usm_array_interface__ support requires DPC backend.", +) +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("dpctl,dpnp", "cpu,gpu") +) +@pytest.mark.parametrize( + "dtype", + [ + pytest.param(np.uint16, id=np.dtype(np.uint16).name), + pytest.param(np.uint32, id=np.dtype(np.uint32).name), + pytest.param(np.uint64, id=np.dtype(np.uint64).name), + ], +) +def test_sua_iface_interop_unsupported_dtypes(dataframe, queue, dtype): + # sua iface interobility supported only for oneDAL supported dtypes + # for input data: int32, int64, float32, float64. + # Checking some common dtypes supported by dpctl, dpnp for exception + # raise. + X = np.zeros((10, 20), dtype=dtype) + X = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) + sua_iface, _, _ = _get_sycl_namespace(X) + + expected_err_msg = "Unable to convert from SUA interface: unknown data type" + with pytest.raises(ValueError, match=expected_err_msg): + to_table(X, sua_iface=sua_iface) + + +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("numpy,dpctl,dpnp", "cpu,gpu") +) +def test_to_table_non_contiguous_input(dataframe, queue): + if dataframe in "dpnp,dpctl" and not _is_dpc_backend: + pytest.skip("__sycl_usm_array_interface__ support requires DPC backend.") + X = np.mgrid[:10, :10] + X = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) + X = X[:, :3] + sua_iface, _, _ = _get_sycl_namespace(X) + # X expected to be non-contiguous. + assert not X.flags.c_contiguous and not X.flags.f_contiguous + + # TODO: + # consistent error message. + if dataframe in "dpnp,dpctl": + expected_err_msg = ( + "Unable to convert from SUA interface: only 1D & 2D tensors are allowed" + ) + else: + expected_err_msg = "Numpy input Could not convert Python object to onedal table." + with pytest.raises(ValueError, match=expected_err_msg): + to_table(X, sua_iface=sua_iface) + + +@pytest.mark.skipif( + _is_dpc_backend, + reason="Required check should be done if no DPC backend.", +) +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("dpctl,dpnp", "cpu,gpu") +) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_sua_iface_interop_if_no_dpc_backend(dataframe, queue, dtype): + X = np.zeros((10, 20), dtype=dtype) + X = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) + sua_iface, _, _ = _get_sycl_namespace(X) + + expected_err_msg = "SYCL usm array conversion to table requires the DPC backend" + with pytest.raises(RuntimeError, match=expected_err_msg): + to_table(X, sua_iface=sua_iface) diff --git a/onedal/datatypes/utils/dtype_conversions.cpp b/onedal/datatypes/utils/dtype_conversions.cpp new file mode 100644 index 0000000000..c3abb966ee --- /dev/null +++ b/onedal/datatypes/utils/dtype_conversions.cpp @@ -0,0 +1,142 @@ +/******************************************************************************* +* Copyright 2024 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. +*******************************************************************************/ + +#include + +#include + +#include "oneapi/dal/common.hpp" +#include "oneapi/dal/detail/common.hpp" + +#include "onedal/datatypes/utils/dtype_conversions.hpp" +#include "onedal/datatypes/utils/dtype_dispatcher.hpp" + +namespace oneapi::dal::python { + +using fwd_map_t = std::unordered_map; +using inv_map_t = std::unordered_map; + +inline void unknown_type() { + throw std::runtime_error("Unknown type"); +} + +// Get the basic type character codes supported on oneDAL backend side +// for a string providing the basic type of the homogeneous array. +template +constexpr inline char type_desc() { + if constexpr (std::is_integral_v) { + if (std::is_unsigned_v) { + // Unsigned integer + return 'u'; + } + else { + // Integer + return 'i'; + } + } + else { + if (std::is_floating_point_v) { + // Floating point + return 'f'; + } + else { + unknown_type(); + } + } +} + +template +constexpr inline char type_size() { + switch (sizeof(Type)) { + case 1ul: return '1'; + case 2ul: return '2'; + case 4ul: return '4'; + case 8ul: return '8'; + default: unknown_type(); + }; +} + +// Get a string encoding elemental data type of the array. +template +inline std::string describe(char e = '<') { + constexpr auto s = type_size(); + constexpr auto d = type_desc(); + return std::string{ { e, d, s } }; +} + +// oneDAL works only on little-endian hardware. +const char end = '<'; + +template +inline auto make_fwd_map(const std::tuple* const = nullptr) { + fwd_map_t result(3ul * sizeof...(Types)); + + dal::detail::apply( + [&](auto type_tag) -> void { + using type_t = std::decay_t; + constexpr auto dal_v = detail::make_data_type(); + result.emplace(describe(end), dal_v); + result.emplace(describe('='), dal_v); + result.emplace(describe('|'), dal_v); + }, + Types{}...); + + return result; +} + +template +inline auto make_inv_map(const std::tuple* const = nullptr) { + inv_map_t result(sizeof...(Types)); + + dal::detail::apply( + [&](auto type_tag) -> void { + using type_t = std::decay_t; + constexpr auto dal_v = detail::make_data_type(); + result.emplace(dal_v, describe('|')); + }, + Types{}...); + + return result; +} + +// The map, that provides translation from `__sycl_usm_array_interface__['typestr']` +// a string encoding elemental data type of the array to oneDAL table data type. +static const fwd_map_t& get_fwd_map() { + constexpr const supported_types_t* types = nullptr; + static const fwd_map_t body = make_fwd_map(types); + return body; +} + +// The map, that provides translation from oneDAL table data type to +// `__sycl_usm_array_interface__['typestr']` a string encoding elemental data type +// of the array. +static const inv_map_t& get_inv_map() { + constexpr const supported_types_t* types = nullptr; + static const inv_map_t body = make_inv_map(types); + return body; +} + +// Convert a string encoding elemental data type of the array to oneDAL homogen table data type. +dal::data_type convert_sua_to_dal_type(std::string dtype) { + return get_fwd_map().at(dtype); +} + +// Convert oneDAL homogen table data type to a string encoding elemental data type of the array. +std::string convert_dal_to_sua_type(dal::data_type dtype) { + return get_inv_map().at(dtype); +} + +} // namespace oneapi::dal::python diff --git a/onedal/datatypes/utils/dtype_conversions.hpp b/onedal/datatypes/utils/dtype_conversions.hpp new file mode 100644 index 0000000000..8b9dd89db6 --- /dev/null +++ b/onedal/datatypes/utils/dtype_conversions.hpp @@ -0,0 +1,53 @@ +/******************************************************************************* +* Copyright 2024 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. +*******************************************************************************/ + +#pragma once + +#include + +#include + +#include "oneapi/dal/common.hpp" + +namespace py = pybind11; + +#define SET_CTYPE_FROM_DAL_TYPE(_T, _FUNCT, _EXCEPTION) \ + switch (_T) { \ + case dal::data_type::float32: { \ + _FUNCT(float); \ + break; \ + } \ + case dal::data_type::float64: { \ + _FUNCT(double); \ + break; \ + } \ + case dal::data_type::int32: { \ + _FUNCT(std::int32_t); \ + break; \ + } \ + case dal::data_type::int64: { \ + _FUNCT(std::int64_t); \ + break; \ + } \ + default: _EXCEPTION; \ + }; + +namespace oneapi::dal::python { + +dal::data_type convert_sua_to_dal_type(std::string dtype); +std::string convert_dal_to_sua_type(dal::data_type dtype); + +} // namespace oneapi::dal::python diff --git a/onedal/datatypes/utils/dtype_dispatcher.hpp b/onedal/datatypes/utils/dtype_dispatcher.hpp new file mode 100644 index 0000000000..d3d2b2e0f3 --- /dev/null +++ b/onedal/datatypes/utils/dtype_dispatcher.hpp @@ -0,0 +1,101 @@ +/******************************************************************************* +* Copyright 2024 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. +*******************************************************************************/ + +#pragma once + +#include +#include + +#include "onedal/common.hpp" +#include "oneapi/dal/common.hpp" +#include "oneapi/dal/detail/common.hpp" + +// TODO: Using includes should be the primary path +#if defined(ONEDAL_VERSION) && (20240400 < ONEDAL_VERSION) + +#include "oneapi/dal/detail/dtype_dispatcher.hpp" + +#else // Version check + +#include "oneapi/dal/detail/error_messages.hpp" + +namespace oneapi::dal::detail { + +template +inline constexpr auto dispatch_by_data_type(data_type dtype, Op&& op, OnUnknown&& on_unknown) { + switch (dtype) { + case data_type::int8: return op(std::int8_t{}); + case data_type::uint8: return op(std::uint8_t{}); + case data_type::int16: return op(std::int16_t{}); + case data_type::uint16: return op(std::uint16_t{}); + case data_type::int32: return op(std::int32_t{}); + case data_type::uint32: return op(std::uint32_t{}); + case data_type::int64: return op(std::int64_t{}); + case data_type::uint64: return op(std::uint64_t{}); + case data_type::float32: return op(float{}); + case data_type::float64: return op(double{}); + default: return on_unknown(dtype); + } +} + +template > +inline constexpr ResultType dispatch_by_data_type(data_type dtype, Op&& op) { + // Necessary to make the return type conformant with + // other dispatch branches + const auto on_unknown = [](data_type) -> ResultType { + using msg = oneapi::dal::detail::error_messages; + throw unimplemented{ msg::unsupported_conversion_types() }; + }; + + return dispatch_by_data_type(dtype, std::forward(op), on_unknown); +} + +} // namespace oneapi::dal::detail + +#endif // Version check + +// TODO: Using includes should be the primary path +#if defined(ONEDAL_VERSION) && (ONEDAL_VERSION < 20240000) + +namespace oneapi::dal::detail { + +template +constexpr inline void apply(Op&& op) { + ((void)op(Types{}), ...); +} + +template +constexpr inline void apply(Op&& op, Args&&... args) { + ((void)op(std::forward(args)), ...); +} + +} //namespace oneapi::dal::detail + +#endif // Version check + +namespace oneapi::dal::python { + +using supported_types_t = std::tuple; +} // namespace oneapi::dal::python diff --git a/onedal/datatypes/numpy_helpers.cpp b/onedal/datatypes/utils/numpy_helpers.cpp similarity index 97% rename from onedal/datatypes/numpy_helpers.cpp rename to onedal/datatypes/utils/numpy_helpers.cpp index 5786f06624..4fb774a6c6 100644 --- a/onedal/datatypes/numpy_helpers.cpp +++ b/onedal/datatypes/utils/numpy_helpers.cpp @@ -14,7 +14,7 @@ * limitations under the License. *******************************************************************************/ -#include "onedal/datatypes/numpy_helpers.hpp" +#include "onedal/datatypes/utils/numpy_helpers.hpp" namespace oneapi::dal::python { diff --git a/onedal/datatypes/numpy_helpers.hpp b/onedal/datatypes/utils/numpy_helpers.hpp similarity index 100% rename from onedal/datatypes/numpy_helpers.hpp rename to onedal/datatypes/utils/numpy_helpers.hpp diff --git a/onedal/datatypes/utils/sua_iface_helpers.cpp b/onedal/datatypes/utils/sua_iface_helpers.cpp new file mode 100644 index 0000000000..60407f5a4b --- /dev/null +++ b/onedal/datatypes/utils/sua_iface_helpers.cpp @@ -0,0 +1,223 @@ +/******************************************************************************* +* Copyright 2024 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. +*******************************************************************************/ + +#ifdef ONEDAL_DATA_PARALLEL +#define NO_IMPORT_ARRAY + +#include +#include +#include + +#include "oneapi/dal/common.hpp" +#include "oneapi/dal/detail/common.hpp" +#include "oneapi/dal/table/homogen.hpp" +#include "oneapi/dal/table/detail/homogen_utils.hpp" + +#include "onedal/common/policy_common.hpp" +#include "onedal/datatypes/data_conversion_sua_iface.hpp" +#include "onedal/datatypes/utils/dtype_conversions.hpp" +#include "onedal/datatypes/utils/dtype_dispatcher.hpp" + +/* __sycl_usm_array_interface__ + * + * Python object representing USM allocations. + * To enable native extensions to pass the memory allocated by a native + * SYCL library to SYCL-aware Python extension without making a copy, + * the class must provide `__sycl_usm_array_interface__` + * attribute which returns a Python dictionary with the following fields: + * + * shape: tuple of int + * typestr: string + * typedescr: a list of tuples + * data: (int, bool) + * strides: tuple of int + * offset: int + * version: int + * syclobj: dpctl.SyclQueue or dpctl.SyclContext object or `SyclQueueRef` PyCapsule that + * represents an opaque value of sycl::queue. + * + * For more informations please follow +*/ + +namespace oneapi::dal::python { + +// Convert a string encoding elemental data type of the array to oneDAL homogen table data type. +dal::data_type get_sua_dtype(const py::dict& sua) { + auto dtype = sua["typestr"].cast(); + return convert_sua_to_dal_type(std::move(dtype)); +} + +// Get `__sycl_usm_array_interface__` dictionary representing USM allocations. +py::dict get_sua_interface(const py::object& obj) { + constexpr const char name[] = "__sycl_usm_array_interface__"; + return obj.attr(name).cast(); +} + +// Get a 2-tuple data entry for `__sycl_usm_array_interface__`. +py::tuple get_sua_data(const py::dict& sua) { + py::tuple result = sua["data"].cast(); + if (result.size() != py::ssize_t{ 2ul }) { + throw std::length_error("Size of \"data\" tuple should be 2"); + } + return result; +} + +// Get `__sycl_usm_array_interface__['data'][0]`, the first element of data entry, +// which is a Python integer encoding USM pointer value. +std::uintptr_t get_sua_ptr(const py::dict& sua) { + const py::tuple data = get_sua_data(sua); + return data[0ul].cast(); +} + +// Get `__sycl_usm_array_interface__['data'][1]`, which is the data entry a read-only +// flag (True means the data area is read-only). +bool is_sua_readonly(const py::dict& sua) { + const py::tuple data = get_sua_data(sua); + return data[1ul].cast(); +} + +// Get `__sycl_usm_array_interface__['shape']`. +// shape : a tuple of integers describing dimensions of an N-dimensional array. +py::tuple get_sua_shape(const py::dict& sua) { + py::tuple shape = sua["shape"].cast(); + if (shape.size() == py::ssize_t{ 0ul }) { + throw std::runtime_error("Wrong number of dimensions"); + } + return shape; +} + +void report_problem_for_sua_iface(const char* clarification) { + constexpr const char* const base_message = "Unable to convert from SUA interface"; + + std::string message{ base_message }; + message += std::string{ clarification }; + throw std::invalid_argument{ message }; +} + +// Get and check `__sycl_usm_array_interface__` number of dimensions. +std::int64_t get_and_check_sua_iface_ndim(const py::dict& sua_dict) { + constexpr const char* const err_message = ": only 1D & 2D tensors are allowed"; + py::tuple shape = get_sua_shape(sua_dict); + const py::ssize_t raw_ndim = shape.size(); + const auto ndim = detail::integral_cast(raw_ndim); + if ((ndim != 1l) && (ndim != 2l)) + report_problem_for_sua_iface(err_message); + return ndim; +} + +// Get the pair of row and column counts. +std::pair get_sua_iface_shape_by_values(const py::dict sua_dict, + const std::int64_t ndim) { + std::int64_t row_count, col_count; + auto shape = sua_dict["shape"].cast(); + if (ndim == 1l) { + row_count = shape[0l].cast(); + col_count = 1l; + } + else { + row_count = shape[0l].cast(); + col_count = shape[1l].cast(); + } + return std::make_pair(row_count, col_count); +} + +// Get oneDAL Homogen DataLayout enumeration from input object shape and strides. +dal::data_layout get_sua_iface_layout(const py::dict& sua_dict, + const std::int64_t& r_count, + const std::int64_t& c_count) { + const auto raw_strides = sua_dict["strides"]; + if (raw_strides.is_none()) { + // None to indicate a C-style contiguous array. + return dal::data_layout::row_major; + } + auto strides_tuple = raw_strides.cast(); + + auto strides_len = py::len(strides_tuple); + + if (strides_len == 1l) { + return dal::data_layout::row_major; + } + else if (strides_len == 2l) { + using tuple_elem_t = std::int64_t; + auto r_strides = strides_tuple[0l].cast(); + auto c_strides = strides_tuple[1l].cast(); + constexpr auto one = static_cast(1); + if (r_strides == c_count && c_strides == one) { + return dal::data_layout::row_major; + } + else if (r_strides == one && c_strides == r_count) { + return dal::data_layout::column_major; + } + else { + throw std::runtime_error("Wrong strides"); + } + } + else { + throw std::runtime_error("Unsupporterd data shape.`"); + } +} + +void report_problem_to_sua_iface(const char* clarification) { + constexpr const char* const base_message = "Unable to convert to SUA interface"; + + std::string message{ base_message }; + message += std::string{ clarification }; + throw std::runtime_error{ message }; +} + +// Get numpy-like strides. Strides is a tuple of integers describing number of array elements +// needed to jump to the next array element in the corresponding dimensions. +py::tuple get_npy_strides(const dal::data_layout& data_layout, + npy_intp row_count, + npy_intp column_count) { + if (data_layout == dal::data_layout::unknown) { + report_problem_to_sua_iface(": unknown data layout"); + } + py::tuple strides; + if (data_layout == dal::data_layout::row_major) { + strides = py::make_tuple(column_count, 1l); + } + else { + strides = py::make_tuple(1l, row_count); + } + return strides; +} + +// Create `SyclQueueRef` PyCapsule that represents an opaque value of +// sycl::queue. +py::capsule pack_queue(const std::shared_ptr& queue) { + static const char queue_capsule_name[] = "SyclQueueRef"; + if (queue.get() == nullptr) { + throw std::runtime_error("Empty queue"); + } + else { + void (*deleter)(void*) = [](void* const queue) -> void { + delete reinterpret_cast(queue); + }; + + sycl::queue* ptr = new sycl::queue{ *queue }; + void* const raw = reinterpret_cast(ptr); + + py::capsule capsule(raw, deleter); + capsule.set_name(queue_capsule_name); + return capsule; + } +} + +} // namespace oneapi::dal::python + +#endif // ONEDAL_DATA_PARALLEL diff --git a/onedal/datatypes/utils/sua_iface_helpers.hpp b/onedal/datatypes/utils/sua_iface_helpers.hpp new file mode 100644 index 0000000000..494ce769e2 --- /dev/null +++ b/onedal/datatypes/utils/sua_iface_helpers.hpp @@ -0,0 +1,69 @@ +/******************************************************************************* +* Copyright 2024 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. +*******************************************************************************/ + +#ifdef ONEDAL_DATA_PARALLEL +#define NO_IMPORT_ARRAY + +#include +#include +#include + +#include "oneapi/dal/common.hpp" +#include "oneapi/dal/detail/common.hpp" +#include "oneapi/dal/table/homogen.hpp" +#include "oneapi/dal/table/detail/homogen_utils.hpp" + +#include "onedal/common/policy_common.hpp" +#include "onedal/datatypes/data_conversion_sua_iface.hpp" +#include "onedal/datatypes/utils/dtype_conversions.hpp" +#include "onedal/datatypes/utils/dtype_dispatcher.hpp" + +namespace oneapi::dal::python { + +dal::data_type get_sua_dtype(const py::dict& sua); + +py::dict get_sua_interface(const py::object& obj); + +py::tuple get_sua_data(const py::dict& sua); + +std::uintptr_t get_sua_ptr(const py::dict& sua); + +bool is_sua_readonly(const py::dict& sua); + +py::tuple get_sua_shape(const py::dict& sua); + +void report_problem_for_sua_iface(const char* clarification); + +std::int64_t get_and_check_sua_iface_ndim(const py::dict& sua_dict); + +std::pair get_sua_iface_shape_by_values(const py::dict sua_dict, + const std::int64_t ndim); + +dal::data_layout get_sua_iface_layout(const py::dict& sua_dict, + const std::int64_t& r_count, + const std::int64_t& c_count); + +void report_problem_to_sua_iface(const char* clarification); + +py::tuple get_npy_strides(const dal::data_layout& data_layout, + npy_intp row_count, + npy_intp column_count); + +py::capsule pack_queue(const std::shared_ptr& queue); + +} // namespace oneapi::dal::python + +#endif // ONEDAL_DATA_PARALLEL diff --git a/onedal/utils/_array_api.py b/onedal/utils/_array_api.py index 8208406a28..47da103da9 100644 --- a/onedal/utils/_array_api.py +++ b/onedal/utils/_array_api.py @@ -63,19 +63,19 @@ def _get_sycl_namespace(*arrays): """Get namespace of sycl arrays.""" # sycl support designed to work regardless of array_api_dispatch sklearn global value - sycl_type = {type(x): x for x in arrays if hasattr(x, "__sycl_usm_array_interface__")} + sua_iface = {type(x): x for x in arrays if hasattr(x, "__sycl_usm_array_interface__")} - if len(sycl_type) > 1: - raise ValueError(f"Multiple SYCL types for array inputs: {sycl_type}") + if len(sua_iface) > 1: + raise ValueError(f"Multiple SYCL types for array inputs: {sua_iface}") - if sycl_type: - (X,) = sycl_type.values() + if sua_iface: + (X,) = sua_iface.values() if hasattr(X, "__array_namespace__"): - return sycl_type, X.__array_namespace__(), True + return sua_iface, X.__array_namespace__(), True elif dpnp_available and isinstance(X, dpnp.ndarray): - return sycl_type, dpnp, False + return sua_iface, dpnp, False else: - raise ValueError(f"SYCL type not recognized: {sycl_type}") + raise ValueError(f"SYCL type not recognized: {sua_iface}") - return sycl_type, None, False + return sua_iface, None, False diff --git a/sklearnex/_device_offload.py b/sklearnex/_device_offload.py index 8b52b3c395..094db9bfd8 100644 --- a/sklearnex/_device_offload.py +++ b/sklearnex/_device_offload.py @@ -22,7 +22,7 @@ _transfer_to_host, dpnp_available, ) -from onedal.utils._array_api import _asarray, _is_numpy_namespace +from onedal.utils._array_api import _asarray if dpnp_available: import dpnp diff --git a/sklearnex/tests/test_memory_usage.py b/sklearnex/tests/test_memory_usage.py index 9c383abaab..7efcca8dfa 100644 --- a/sklearnex/tests/test_memory_usage.py +++ b/sklearnex/tests/test_memory_usage.py @@ -23,7 +23,6 @@ from inspect import isclass import numpy as np -import pandas as pd import pytest from scipy.stats import pearsonr from sklearn.base import BaseEstimator, clone @@ -36,10 +35,18 @@ get_dataframes_and_queues, ) from onedal.tests.utils._device_selection import get_queues, is_dpctl_device_available +from onedal.utils._array_api import _get_sycl_namespace +from onedal.utils._dpep_helpers import dpctl_available, dpnp_available, is_dpctl_available from sklearnex import config_context from sklearnex.tests.utils import PATCHED_FUNCTIONS, PATCHED_MODELS, SPECIAL_INSTANCES from sklearnex.utils._array_api import get_namespace +if dpctl_available: + from dpctl.tensor import usm_ndarray + +if dpnp_available: + import dpnp + if _is_dpc_backend: from onedal import _backend @@ -125,10 +132,47 @@ def gen_functions(functions): ORDER_DICT = {"F": np.asfortranarray, "C": np.ascontiguousarray} -def gen_clsf_data(n_samples, n_features): +if _is_dpc_backend: + + from sklearn.utils.validation import check_is_fitted + + from onedal.datatypes import from_table, to_table + + class DummyEstimatorWithTableConversions(BaseEstimator): + + def fit(self, X, y=None): + sua_iface, xp, _ = _get_sycl_namespace(X) + X_table = to_table(X, sua_iface=sua_iface) + y_table = to_table(y, sua_iface=sua_iface) + # The presence of the fitted attributes (ending with a trailing + # underscore) is required for the correct check. The cleanup of + # the memory will occur at the estimator instance deletion. + self.x_attr_ = from_table( + X_table, sua_iface=sua_iface, sycl_queue=X.sycl_queue, xp=xp + ) + self.y_attr_ = from_table( + y_table, sua_iface=sua_iface, sycl_queue=X.sycl_queue, xp=xp + ) + return self + + def predict(self, X): + # Checks if the estimator is fitted by verifying the presence of + # fitted attributes (ending with a trailing underscore). + check_is_fitted(self) + sua_iface, xp, _ = _get_sycl_namespace(X) + X_table = to_table(X, sua_iface=sua_iface) + returned_X = from_table( + X_table, sua_iface=sua_iface, sycl_queue=X.sycl_queue, xp=xp + ) + return returned_X + + +def gen_clsf_data(n_samples, n_features, dtype=None): data, label = make_classification( n_classes=2, n_samples=n_samples, n_features=n_features, random_state=777 ) + if dtype: + data, label = data.astype(dtype), label.astype(dtype) return ( data, label, @@ -145,8 +189,18 @@ def get_traced_memory(queue=None): def take(x, index, axis=0, queue=None): xp, array_api = get_namespace(x) - if array_api: - return xp.take(x, xp.asarray(index, device=queue), axis=axis) + if ( + dpnp_available + and isinstance(x, dpnp.ndarray) + or dpctl_available + and isinstance(x, usm_ndarray) + ): + # Using the same sycl queue for dpnp.ndarray or usm_ndarray. + return xp.take( + x, xp.asarray(index, usm_type="device", sycl_queue=x.sycl_queue), axis=axis + ) + elif array_api: + return xp.take(x, xp.asarray(index, device=x.device), axis=axis) else: return x.take(index, axis=axis) @@ -185,11 +239,13 @@ def split_train_inference(kf, x, y, estimator, queue=None): return mem_tracks -def _kfold_function_template(estimator, dataframe, data_shape, queue=None, func=None): +def _kfold_function_template( + estimator, dataframe, data_shape, queue=None, func=None, dtype=None +): tracemalloc.start() n_samples, n_features = data_shape - X, y, data_memory_size = gen_clsf_data(n_samples, n_features) + X, y, data_memory_size = gen_clsf_data(n_samples, n_features, dtype=dtype) kf = KFold(n_splits=N_SPLITS) if func: X = func(X) @@ -293,3 +349,31 @@ def test_gpu_memory_leaks(estimator, queue, order, data_shape): with config_context(target_offload=queue): _kfold_function_template(GPU_ESTIMATORS[estimator], None, data_shape, queue, func) + + +@pytest.mark.skipif( + not _is_dpc_backend, + reason="__sycl_usm_array_interface__ support requires DPC backend.", +) +@pytest.mark.parametrize( + "dataframe,queue", get_dataframes_and_queues("dpctl,dpnp", "cpu,gpu") +) +@pytest.mark.parametrize("order", ["F", "C"]) +@pytest.mark.parametrize("data_shape", data_shapes) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_table_conversions_memory_leaks(dataframe, queue, order, data_shape, dtype): + func = ORDER_DICT[order] + + if queue.sycl_device.is_gpu and ( + os.getenv("ZES_ENABLE_SYSMAN") is None or not is_dpctl_available("gpu") + ): + pytest.skip("SYCL device memory leak check requires the level zero sysman") + + _kfold_function_template( + DummyEstimatorWithTableConversions, + dataframe, + data_shape, + queue, + func, + dtype, + )