From 6d4e02c4a05e01419ab407c17b62e9550bd8ea2b Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Wed, 5 Aug 2026 22:42:53 -0400 Subject: [PATCH 1/4] fix numpy copy for gpus followed the other functions' way of copying --- src/PyOmega_h_numpy_transform.hpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/PyOmega_h_numpy_transform.hpp b/src/PyOmega_h_numpy_transform.hpp index 6cc14801..bacdeac4 100644 --- a/src/PyOmega_h_numpy_transform.hpp +++ b/src/PyOmega_h_numpy_transform.hpp @@ -17,10 +17,12 @@ Omega_h::Read numpy_to_omega_h_read(py::array_t arr) if (buf.ndim != 1) { throw std::runtime_error("Number of dimensions must be 1"); } - Kokkos::View> - view(reinterpret_cast(buf.ptr), buf.shape[0]); - Omega_h::Write write_view(view); + auto write_view_host = Omega_h::HostWrite(buf.shape[0]); + T* ptr = static_cast(buf.ptr); + for (Omega_h::LO i = 0; i < buf.shape[0]; ++i) { + write_view_host[i] = ptr[i]; + } + auto write_view = Omega_h::Write(write_view_host); Omega_h::Read read_view(write_view); return read_view; } @@ -57,16 +59,20 @@ Omega_h::Write numpy_to_omega_h_write(py::array_t arr) return write_view; } -// Helper to convert Omega_h::Write to numpy array (creates a reference) +// Helper to convert Omega_h::Write to numpy array (creates a copy) template py::array_t omega_h_write_to_numpy(Omega_h::Write write_view) { - return py::array_t({static_cast(write_view.size())}, // shape - {sizeof(T)}, // strides - write_view.data(), // data pointer - py::cast(write_view) // base object to manage lifetime - ); + auto write_view_host = Omega_h::HostWrite(write_view); + py::array_t result(write_view_host.size()); + py::buffer_info buf = result.request(); + T* ptr = static_cast(buf.ptr); + for (Omega_h::LO i = 0; i < write_view_host.size(); ++i) { + ptr[i] = write_view_host[i]; + } + return result; } } // namespace Omega_h -#endif \ No newline at end of file +#endif + From 03fd53c3533d5a7cdbb09ca97014f2b0db641de2 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Wed, 5 Aug 2026 23:43:02 -0400 Subject: [PATCH 2/4] test numpy copies --- pytest/test_numpy_transform.py | 50 ++++++++++++++++++++++++++++++++++ src/PyOmega_h_array.cpp | 12 ++++++++ 2 files changed, 62 insertions(+) create mode 100644 pytest/test_numpy_transform.py diff --git a/pytest/test_numpy_transform.py b/pytest/test_numpy_transform.py new file mode 100644 index 00000000..8d9b8178 --- /dev/null +++ b/pytest/test_numpy_transform.py @@ -0,0 +1,50 @@ +""" +Direct unit tests for numpy_to_omega_h_read and omega_h_write_to_numpy. + +Verifies that data is properly deep-copied through host memory +(not aliased from numpy buffer or device pointer). + +Run with: pytest test_numpy_transform.py -v +""" + +import numpy as np +import PyOmega_h as omega_h + + +def test_numpy_to_read_copies_data(): + """numpy_to_omega_h_read must copy data through host, not alias.""" + size = 10 + original = np.arange(size, dtype=np.float64) + read_view = omega_h.numpy_to_read_float64(original) + + # Access Read data via HostRead (buffer protocol) + host_read = omega_h.HostRead_float64(read_view) + result = np.array(host_read, copy=False) + np.testing.assert_array_equal(result, original) + + # Mutate original — result must be unchanged (deep copy) + original[:] = -999.0 + result_after = np.array(host_read, copy=False) + expected = np.arange(size, dtype=np.float64) + np.testing.assert_array_equal(result_after, expected) + + +def test_write_to_numpy_copies_data(): + """omega_h_write_to_numpy must copy device data to host numpy array.""" + size = 10 + expected = np.arange(size, dtype=np.float64) + + host_write = omega_h.HostWrite_float64(size) + # Fill via buffer protocol + view = np.array(host_write, copy=False) + view[:] = expected + + # HostWrite.write() returns Write; convert to numpy + write_view = host_write.write() + result = omega_h.write_to_numpy_float64(write_view) + np.testing.assert_array_equal(result, expected) + + # Mutate host — result must be unchanged (deep copy) + view[:] = -999.0 + np.testing.assert_array_equal(result, expected) + diff --git a/src/PyOmega_h_array.cpp b/src/PyOmega_h_array.cpp index aff9ca28..2802701a 100644 --- a/src/PyOmega_h_array.cpp +++ b/src/PyOmega_h_array.cpp @@ -1,5 +1,6 @@ #include #include +#include namespace Omega_h { @@ -48,6 +49,17 @@ static void pybind11_array_type(py::module& m, &deep_copy; m.def(deepcopy_name.c_str(), deep_copy_type, py::arg("a"), py::arg("name") = ""); + // Expose numpy transform functions for direct testing + m.def(("numpy_to_read_" + py_scalar).c_str(), + [](py::array_t arr) { return numpy_to_omega_h_read(arr); }, + py::arg("arr"), + "Convert 1D numpy array to Read array (copies through host)"); + m.def(("write_to_numpy_" + py_scalar).c_str(), + [](Write w) { + return omega_h_write_to_numpy(w); + }, + py::arg("write_view"), + "Convert Write array to numpy array (copies through host)"); } void pybind11_array(py::module& m) { From 740b2b11f9fa21af27799f6b8d82df39fc820fd2 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Thu, 6 Aug 2026 15:17:07 -0400 Subject: [PATCH 3/4] comment reason for host-host copy --- src/PyOmega_h_numpy_transform.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PyOmega_h_numpy_transform.hpp b/src/PyOmega_h_numpy_transform.hpp index bacdeac4..975e6ccb 100644 --- a/src/PyOmega_h_numpy_transform.hpp +++ b/src/PyOmega_h_numpy_transform.hpp @@ -19,6 +19,8 @@ Omega_h::Read numpy_to_omega_h_read(py::array_t arr) } auto write_view_host = Omega_h::HostWrite(buf.shape[0]); T* ptr = static_cast(buf.ptr); + // copying to hostwrite instead of wrapping numpy pointer + // to avoid issues with sliced numpy arrays for (Omega_h::LO i = 0; i < buf.shape[0]; ++i) { write_view_host[i] = ptr[i]; } @@ -49,9 +51,10 @@ Omega_h::Write numpy_to_omega_h_write(py::array_t arr) if (buf.ndim != 1) { throw std::runtime_error("Number of dimensions must be 1"); } - // Get host mirror and copy data auto write_view_host = Omega_h::HostWrite(buf.shape[0]); T* ptr = static_cast(buf.ptr); + // copying to hostwrite instead of wrapping numpy pointer + // to avoid issues with sliced numpy arrays for (Omega_h::LO i = 0; i < buf.shape[0]; ++i) { write_view_host[i] = ptr[i]; } From cae64f9ff2140b55f5e3c8ce071f5847a6f4a408 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Thu, 6 Aug 2026 15:59:48 -0400 Subject: [PATCH 4/4] handle strided numpy array and test --- pytest/test_numpy_transform.py | 54 +++++++++++++++++++++++++++++++ src/PyOmega_h_numpy_transform.hpp | 18 +++++++---- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/pytest/test_numpy_transform.py b/pytest/test_numpy_transform.py index 8d9b8178..4b6ae35f 100644 --- a/pytest/test_numpy_transform.py +++ b/pytest/test_numpy_transform.py @@ -48,3 +48,57 @@ def test_write_to_numpy_copies_data(): view[:] = -999.0 np.testing.assert_array_equal(result, expected) + +def test_numpy_to_read_sliced_step(): + """numpy_to_omega_h_read must handle non-contiguous sliced arrays.""" + base = np.arange(20, dtype=np.float64) + sliced = base[::3] # [0, 3, 6, 9, 12, 15, 18] — non-contiguous + assert not sliced.flags["C_CONTIGUOUS"] + + read_view = omega_h.numpy_to_read_float64(sliced) + host_read = omega_h.HostRead_float64(read_view) + result = np.array(host_read, copy=False) + np.testing.assert_array_equal(result, sliced) + + # Verify deep copy — mutating base doesn't affect result + base[:] = -999.0 + result_after = np.array(host_read, copy=False) + expected = np.array([0, 3, 6, 9, 12, 15, 18], dtype=np.float64) + np.testing.assert_array_equal(result_after, expected) + + +def test_numpy_to_read_sliced_reversed(): + """numpy_to_omega_h_read must handle reversed (negative stride) arrays.""" + base = np.array([1, 2, 3, 4, 5], dtype=np.int32) + sliced = base[::-1] # [5, 4, 3, 2, 1] — negative stride + assert not sliced.flags["C_CONTIGUOUS"] + + read_view = omega_h.numpy_to_read_int32(sliced) + host_read = omega_h.HostRead_int32(read_view) + result = np.array(host_read, copy=False) + np.testing.assert_array_equal(result, sliced) + + # Verify deep copy + base[:] = 0 + result_after = np.array(host_read, copy=False) + expected = np.array([5, 4, 3, 2, 1], dtype=np.int32) + np.testing.assert_array_equal(result_after, expected) + + +def test_numpy_to_read_sliced_offset(): + """numpy_to_omega_h_read must handle sliced arrays with offset + step.""" + base = np.array([10, 11, 12, 13, 14, 15, 16], dtype=np.int64) + sliced = base[2:6:2] # [12, 14] — offset by 2, step 2 + assert not sliced.flags["C_CONTIGUOUS"] + + read_view = omega_h.numpy_to_read_int64(sliced) + host_read = omega_h.HostRead_int64(read_view) + result = np.array(host_read, copy=False) + np.testing.assert_array_equal(result, sliced) + + # Verify deep copy + base[2] = 999 + result_after = np.array(host_read, copy=False) + expected = np.array([12, 14], dtype=np.int64) + np.testing.assert_array_equal(result_after, expected) + diff --git a/src/PyOmega_h_numpy_transform.hpp b/src/PyOmega_h_numpy_transform.hpp index 975e6ccb..e81d7ea9 100644 --- a/src/PyOmega_h_numpy_transform.hpp +++ b/src/PyOmega_h_numpy_transform.hpp @@ -17,12 +17,15 @@ Omega_h::Read numpy_to_omega_h_read(py::array_t arr) if (buf.ndim != 1) { throw std::runtime_error("Number of dimensions must be 1"); } + // Copy element-by-element through HostWrite instead of wrapping + // the numpy pointer directly in a Kokkos view. This ensures: + // 1. Proper memory ownership (Write owns its allocation even with kokkos CPU backend) + // 2. Correct handling of non-contiguous (sliced) numpy arrays via stride auto write_view_host = Omega_h::HostWrite(buf.shape[0]); T* ptr = static_cast(buf.ptr); - // copying to hostwrite instead of wrapping numpy pointer - // to avoid issues with sliced numpy arrays + ssize_t stride = buf.strides[0] / sizeof(T); for (Omega_h::LO i = 0; i < buf.shape[0]; ++i) { - write_view_host[i] = ptr[i]; + write_view_host[i] = ptr[i * stride]; } auto write_view = Omega_h::Write(write_view_host); Omega_h::Read read_view(write_view); @@ -51,12 +54,15 @@ Omega_h::Write numpy_to_omega_h_write(py::array_t arr) if (buf.ndim != 1) { throw std::runtime_error("Number of dimensions must be 1"); } + // Copy element-by-element through HostWrite instead of wrapping + // the numpy pointer directly in a Kokkos view. This ensures: + // 1. Proper memory ownership (Write owns its allocation even with kokkos CPU backend) + // 2. Correct handling of non-contiguous (sliced) numpy arrays via stride auto write_view_host = Omega_h::HostWrite(buf.shape[0]); T* ptr = static_cast(buf.ptr); - // copying to hostwrite instead of wrapping numpy pointer - // to avoid issues with sliced numpy arrays + ssize_t stride = buf.strides[0] / sizeof(T); for (Omega_h::LO i = 0; i < buf.shape[0]; ++i) { - write_view_host[i] = ptr[i]; + write_view_host[i] = ptr[i * stride]; } auto write_view = Omega_h::Write(write_view_host); return write_view;