diff --git a/onedal/datatypes/sycl_usm/data_conversion.cpp b/onedal/datatypes/sycl_usm/data_conversion.cpp index 21eb0c96b9..51a64a4bfd 100644 --- a/onedal/datatypes/sycl_usm/data_conversion.cpp +++ b/onedal/datatypes/sycl_usm/data_conversion.cpp @@ -86,8 +86,11 @@ dal::table convert_to_homogen_impl(py::object obj) { } // 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)); + // which is a Python integer encoding USM pointer value. The `offset` field (in + // elements) is applied because sliced views keep the base allocation pointer in + // `data[0]` and encode their start position via the offset instead. + const auto offset = get_sua_offset(sua_iface_dict); + const auto* const ptr = reinterpret_cast(get_sua_ptr(sua_iface_dict)) + offset; // Get SYCL object from `__sycl_usm_array_interface__["syclobj"]`. // syclobj: Python object from which SYCL context to which represented USM diff --git a/onedal/datatypes/sycl_usm/sycl_usm_utils.cpp b/onedal/datatypes/sycl_usm/sycl_usm_utils.cpp index 2b624d8599..982bc5e6f9 100644 --- a/onedal/datatypes/sycl_usm/sycl_usm_utils.cpp +++ b/onedal/datatypes/sycl_usm/sycl_usm_utils.cpp @@ -89,6 +89,17 @@ bool is_sua_readonly(const py::dict& sua) { return data[1ul].cast(); } +// Get `__sycl_usm_array_interface__['offset']`, the offset in elements from the start +// of the USM allocation to the first array element. The field is optional and defaults +// to zero; sliced views (e.g. dpnp X[a:b]) report a non-zero offset while keeping the +// same base pointer in `data[0]`, so it must be applied to avoid reading the wrong rows. +std::int64_t get_sua_offset(const py::dict& sua) { + if (!sua.contains("offset") || sua["offset"].is_none()) { + return 0l; + } + return sua["offset"].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) { diff --git a/onedal/datatypes/sycl_usm/sycl_usm_utils.hpp b/onedal/datatypes/sycl_usm/sycl_usm_utils.hpp index 75834bedef..cbcad3b9c1 100644 --- a/onedal/datatypes/sycl_usm/sycl_usm_utils.hpp +++ b/onedal/datatypes/sycl_usm/sycl_usm_utils.hpp @@ -42,6 +42,8 @@ std::uintptr_t get_sua_ptr(const py::dict& sua); bool is_sua_readonly(const py::dict& sua); +std::int64_t get_sua_offset(const py::dict& sua); + py::tuple get_sua_shape(const py::dict& sua); void report_problem_for_sua_iface(const char* clarification); diff --git a/onedal/datatypes/tests/test_data.py b/onedal/datatypes/tests/test_data.py index 78950425b3..771cf14400 100644 --- a/onedal/datatypes/tests/test_data.py +++ b/onedal/datatypes/tests/test_data.py @@ -40,6 +40,7 @@ from onedal.cluster.dbscan import DBSCAN from onedal.primitives import linear_kernel from onedal.tests.utils._dataframes_support import ( + _as_numpy, _convert_to_dataframe, array_api_modules, get_dataframes_and_queues, @@ -228,6 +229,43 @@ def test_input_zero_copy_sycl_usm(dataframe, queue, order, dtype): _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 backend.is_dpc, + reason="__sycl_usm_array_interface__ support requires DPC backend.", +) +@pytest.mark.parametrize("dataframe,queue", get_dataframes_and_queues("dpnp", "cpu,gpu")) +@pytest.mark.parametrize( + "slicer", + [ + pytest.param(np.s_[3:], id="rows"), + pytest.param(np.s_[2:8], id="row_range"), + pytest.param(np.s_[::2], id="row_step"), + pytest.param(np.s_[:, 1:4], id="cols"), + pytest.param(np.s_[:, 2:3], id="single_col"), + pytest.param(np.s_[2:8, 1:4], id="row_and_col"), + ], +) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_sliced_view_offset_sycl_usm(dataframe, queue, slicer, dtype): + """A sliced view keeps the base allocation pointer in + `__sycl_usm_array_interface__['data'][0]` and encodes its start via the + `offset` field. The table conversion must apply that offset, otherwise the + view reads from row 0 (regression test for the SUA offset bug). Non-unit + steps and column subsets additionally exercise non-contiguous views. + """ + X_np = np.arange(50, dtype=dtype).reshape(10, 5) + X_dp = _convert_to_dataframe(X_np, sycl_queue=queue, target_df=dataframe) + + X_view = X_dp[slicer] + X_roundtrip = from_table(to_table(X_view), like=X_dp) + + assert_allclose(_as_numpy(X_roundtrip), X_np[slicer]) + + @pytest.mark.skipif( not dpctl_available, reason="dpctl is required for checks.",