core
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
ndarray_interop.hpp
Go to the documentation of this file.
1// core — shared zero-copy bridge between Kokkos Views and Python arrays (nanobind).
2//
3// One place defines how every Kokkos-backed Python binding in the suite (flow, dem, voro,
4// core's own tpx_amr) moves arrays across the C++/Python boundary, so the array
5// contract — shapes, strides, dtype, host-vs-device — is identical everywhere instead of being
6// re-hand-rolled (the old per-module `to_xyz`/`to_vec`/`upload` helpers).
7//
8// The win over the retired pybind11 path is that nanobind's `nb::ndarray` carries a DLPack device
9// tag and arbitrary strides, so:
10// * a host View exports as a NumPy array that *references* the View's memory (no copy), and
11// * a device (CUDA/HIP) View exports as a DLPack-capable array that CuPy/PyTorch consume
12// zero-copy via `cupy.from_dlpack(...)` / `torch.from_dlpack(...)`.
13// Lifetime is correct because the exported array owns a capsule holding a *copy* of the View, and
14// Kokkos Views are reference-counted — the allocation lives exactly as long as Python references
15// it.
16//
17// This header is only included by binding translation units (which link nanobind + Kokkos). It is
18// NOT pulled into the device kernels. Layout note: the suite is x-fastest (LayoutLeft), so a
19// `peclet::core::Field3D<T>` of logical shape (nx,ny,nz) exports with element strides {1, nx,
20// nx*ny} — i.e. a Fortran-order NumPy array indexed [x,y,z], matching docs/CONVENTIONS.md §6.
21#ifndef PECLET_CORE_PYTHON_NDARRAY_INTEROP_HPP
22#define PECLET_CORE_PYTHON_NDARRAY_INTEROP_HPP
23
24#include <nanobind/nanobind.h>
25#include <nanobind/ndarray.h>
26
27#include <array>
28#include <cstring>
29#include <Kokkos_Core.hpp>
30#include <stdexcept>
31#include <type_traits>
32#include <utility>
33#include <vector>
34
36
38
39namespace nb = nanobind;
40
43template <class MemorySpace>
44inline constexpr bool is_host_space_v =
45 Kokkos::SpaceAccessibility<Kokkos::HostSpace, MemorySpace>::accessible;
46
49template <class MemorySpace>
50inline std::pair<int, int> dlpack_device() {
51 if constexpr (is_host_space_v<MemorySpace>) {
52 return {nb::device::cpu::value, 0};
53 } else {
54#if defined(KOKKOS_ENABLE_HIP)
55 return {nb::device::rocm::value, Kokkos::device_id()};
56#else
57 return {nb::device::cuda::value, Kokkos::device_id()};
58#endif
59 }
60}
61
63template <class T>
64inline void require_dtype(const nb::ndarray<>& a, const char* who) {
65 if (a.dtype() != nb::dtype<T>())
66 throw std::runtime_error(std::string(who) + ": array dtype does not match the expected scalar");
67}
68
75template <class V>
76auto view_to_ndarray(const V& view) {
77 using T = std::remove_const_t<typename V::value_type>;
78 using Mem = typename V::memory_space;
79 constexpr std::size_t N = V::rank;
80
81 std::array<std::size_t, N> shape;
82 std::array<std::int64_t, N> strides;
83 for (std::size_t i = 0; i < N; ++i) {
84 shape[i] = view.extent(i);
85 strides[i] = static_cast<std::int64_t>(view.stride(i));
86 }
87
88 // Capsule owns a heap copy of the View; Kokkos ref-counts the allocation, so the buffer lives as
89 // long as the Python array (or any array derived from it) does.
90 auto* held = new V(view);
91 nb::capsule owner(held, [](void* p) noexcept { delete static_cast<V*>(p); });
92
93 auto* data = const_cast<T*>(view.data());
94 if constexpr (is_host_space_v<Mem>) {
95 return nb::ndarray<nb::numpy, T>(data, N, shape.data(), owner, strides.data(), nb::dtype<T>(),
96 nb::device::cpu::value, 0);
97 } else {
98 auto [dev, id] = dlpack_device<Mem>();
99 return nb::ndarray<T>(data, N, shape.data(), owner, strides.data(), nb::dtype<T>(), dev, id);
100 }
101}
102
107template <class T>
108nb::ndarray<nb::numpy, T> vector_to_ndarray(std::vector<T>&& v,
109 std::initializer_list<std::size_t> shape,
110 std::initializer_list<std::int64_t> strides) {
111 auto* held = new std::vector<T>(std::move(v));
112 nb::capsule owner(held, [](void* p) noexcept { delete static_cast<std::vector<T>*>(p); });
113 return nb::ndarray<nb::numpy, T>(held->data(), shape, owner, strides, nb::dtype<T>(),
114 nb::device::cpu::value, 0);
115}
116
124template <class T>
125peclet::core::View<T> ndarray_to_view(const nb::ndarray<>& a, const std::string& label) {
126 require_dtype<T>(a, "ndarray_to_view");
127 const std::size_t n = a.size();
128 peclet::core::View<T> d(Kokkos::view_alloc(label, Kokkos::WithoutInitializing), n);
129 const int dev = a.device_type();
130 if (dev == nb::device::cpu::value) {
131 Kokkos::View<const T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(
132 static_cast<const T*>(a.data()), n);
133 Kokkos::deep_copy(d, hv);
134 } else if constexpr (!is_host_space_v<peclet::core::MemSpace>) {
136 throw std::runtime_error(
137 "ndarray_to_view: array is on an incompatible device for this build");
138 Kokkos::View<const T*, peclet::core::MemSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> dv(
139 static_cast<const T*>(a.data()), n);
140 Kokkos::deep_copy(d, dv);
141 } else {
142 throw std::runtime_error("ndarray_to_view: array is on an incompatible device for this build");
143 }
144 return d;
145}
146
151template <class T>
152std::vector<T> ndarray_to_vector(const nb::ndarray<>& a) {
153 require_dtype<T>(a, "ndarray_to_vector");
154 const std::size_t n = a.size();
155 std::vector<T> out(n);
156 const int dev = a.device_type();
157 if (dev == nb::device::cpu::value) {
158 if (n)
159 std::memcpy(out.data(), a.data(), n * sizeof(T));
160 } else if constexpr (!is_host_space_v<peclet::core::MemSpace>) {
162 throw std::runtime_error(
163 "ndarray_to_vector: array is on an incompatible device for this build");
164 Kokkos::View<const T*, peclet::core::MemSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> dv(
165 static_cast<const T*>(a.data()), n);
166 Kokkos::View<T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(out.data(), n);
167 Kokkos::deep_copy(hv, dv);
168 } else {
169 throw std::runtime_error(
170 "ndarray_to_vector: array is on an incompatible device for this build");
171 }
172 return out;
173}
174
175} // namespace peclet::core::python
176
177#endif // PECLET_CORE_PYTHON_NDARRAY_INTEROP_HPP
nb::ndarray< nb::numpy, T > vector_to_ndarray(std::vector< T > &&v, std::initializer_list< std::size_t > shape, std::initializer_list< std::int64_t > strides)
Export a host std::vector<T> as a NumPy array of the given logical shape and element-unit strides,...
peclet::core::View< T > ndarray_to_view(const nb::ndarray<> &a, const std::string &label)
Import a (contiguous) Python array into a freshly-allocated flat device peclet::core::View<T>.
auto view_to_ndarray(const V &view)
Export a Kokkos View to a Python array without copying.
std::vector< T > ndarray_to_vector(const nb::ndarray<> &a)
Import a (contiguous) Python array into a host std::vector<T>.
void require_dtype(const nb::ndarray<> &a, const char *who)
Throw if a's element type is not T.
constexpr bool is_host_space_v
True when MemorySpace is reachable from the host (NumPy export path); false for a device-resident spa...
std::pair< int, int > dlpack_device()
Map a Kokkos memory space to a DLPack {device_type, device_id} pair.
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26