core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (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 peclet.core.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. The capsule is also a `Releasable` (kokkos_teardown.hpp): the module's shutdown drops its
16// View before Kokkos::finalize, so an array still referenced at interpreter exit (a notebook
17// global) can no longer abort the process with "deallocated after Kokkos::finalize".
18//
19// This header is only included by binding translation units (which link nanobind + Kokkos). It is
20// NOT pulled into the device kernels. Layout note: the suite is x-fastest (LayoutLeft), so a
21// `peclet::core::Field3D<T>` of logical shape (nx,ny,nz) exports with element strides {1, nx,
22// nx*ny} — i.e. a Fortran-order NumPy array indexed [x,y,z], matching docs/CONVENTIONS.md §6.
23#ifndef PECLET_CORE_PYTHON_NDARRAY_INTEROP_HPP
24#define PECLET_CORE_PYTHON_NDARRAY_INTEROP_HPP
25
26#include <nanobind/nanobind.h>
27#include <nanobind/ndarray.h>
28
29#include <array>
30#include <cstring>
31#include <Kokkos_Core.hpp>
32#include <stdexcept>
33#include <type_traits>
34#include <utility>
35#include <vector>
36
39
40namespace peclet::core::python {
41
42namespace nb = nanobind;
43
46template <class MemorySpace>
47inline constexpr bool is_host_space_v =
48 Kokkos::SpaceAccessibility<Kokkos::HostSpace, MemorySpace>::accessible;
49
52template <class MemorySpace>
53inline std::pair<int, int> dlpack_device() {
54 if constexpr (is_host_space_v<MemorySpace>) {
55 return {nb::device::cpu::value, 0};
56 } else {
57#if defined(KOKKOS_ENABLE_HIP)
58 return {nb::device::rocm::value, Kokkos::device_id()};
59#else
60 return {nb::device::cuda::value, Kokkos::device_id()};
61#endif
62 }
63}
64
66template <class T>
67inline void require_dtype(const nb::ndarray<>& a, const char* who) {
68 if (a.dtype() != nb::dtype<T>())
69 throw std::runtime_error(std::string(who) + ": array dtype does not match the expected scalar");
70}
71
76template <class V>
79 explicit ViewCapsule(const V& v) : view(v) {}
80 void release() noexcept override { view = V{}; }
81};
82
88template <class V, std::size_t N>
89auto view_to_ndarray(const V& view, const std::array<std::size_t, N>& shape,
90 const std::array<std::int64_t, N>& strides) {
91 using T = std::remove_const_t<typename V::value_type>;
92 using Mem = typename V::memory_space;
93
94 auto* held = new ViewCapsule<V>(view);
95 nb::capsule owner(held, [](void* p) noexcept { delete static_cast<ViewCapsule<V>*>(p); });
96
97 auto* data = const_cast<T*>(view.data());
98 if constexpr (is_host_space_v<Mem>) {
99 return nb::ndarray<nb::numpy, T>(data, N, shape.data(), owner, strides.data(), nb::dtype<T>(),
100 nb::device::cpu::value, 0);
101 } else {
102 auto [dev, id] = dlpack_device<Mem>();
103 return nb::ndarray<T>(data, N, shape.data(), owner, strides.data(), nb::dtype<T>(), dev, id);
104 }
105}
106
111template <class V>
112auto view_to_ndarray(const V& view) {
113 constexpr std::size_t N = V::rank;
114 std::array<std::size_t, N> shape;
115 std::array<std::int64_t, N> strides;
116 for (std::size_t i = 0; i < N; ++i) {
117 shape[i] = view.extent(i);
118 strides[i] = static_cast<std::int64_t>(view.stride(i));
119 }
120 return view_to_ndarray(view, shape, strides);
121}
122
127template <class T>
128nb::ndarray<nb::numpy, T> vector_to_ndarray(std::vector<T>&& v,
129 std::initializer_list<std::size_t> shape,
130 std::initializer_list<std::int64_t> strides) {
131 auto* held = new std::vector<T>(std::move(v));
132 nb::capsule owner(held, [](void* p) noexcept { delete static_cast<std::vector<T>*>(p); });
133 return nb::ndarray<nb::numpy, T>(held->data(), shape, owner, strides, nb::dtype<T>(),
134 nb::device::cpu::value, 0);
135}
136
144template <class T>
145peclet::core::View<T> ndarray_to_view(const nb::ndarray<>& a, const std::string& label) {
146 require_dtype<T>(a, "ndarray_to_view");
147 const std::size_t n = a.size();
148 peclet::core::View<T> d(Kokkos::view_alloc(label, Kokkos::WithoutInitializing), n);
149 const int dev = a.device_type();
150 if (dev == nb::device::cpu::value) {
151 Kokkos::View<const T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(
152 static_cast<const T*>(a.data()), n);
153 Kokkos::deep_copy(d, hv);
154 } else if constexpr (!is_host_space_v<peclet::core::MemSpace>) {
156 throw std::runtime_error(
157 "ndarray_to_view: array is on an incompatible device for this build");
158 Kokkos::View<const T*, peclet::core::MemSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> dv(
159 static_cast<const T*>(a.data()), n);
160 Kokkos::deep_copy(d, dv);
161 } else {
162 throw std::runtime_error("ndarray_to_view: array is on an incompatible device for this build");
163 }
164 return d;
165}
166
171template <class T>
172std::vector<T> ndarray_to_vector(const nb::ndarray<>& a) {
173 require_dtype<T>(a, "ndarray_to_vector");
174 const std::size_t n = a.size();
175 std::vector<T> out(n);
176 const int dev = a.device_type();
177 if (dev == nb::device::cpu::value) {
178 if (n)
179 std::memcpy(out.data(), a.data(), n * sizeof(T));
180 } else if constexpr (!is_host_space_v<peclet::core::MemSpace>) {
182 throw std::runtime_error(
183 "ndarray_to_vector: array is on an incompatible device for this build");
184 Kokkos::View<const T*, peclet::core::MemSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> dv(
185 static_cast<const T*>(a.data()), n);
186 Kokkos::View<T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(out.data(), n);
187 Kokkos::deep_copy(hv, dv);
188 } else {
189 throw std::runtime_error(
190 "ndarray_to_vector: array is on an incompatible device for this build");
191 }
192 return out;
193}
194
195} // namespace peclet::core::python
196
197#endif // PECLET_CORE_PYTHON_NDARRAY_INTEROP_HPP
Base of everything on the binding side that owns Kokkos Views on Python's behalf.
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>.
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.
auto view_to_ndarray(const V &view, const std::array< std::size_t, N > &shape, const std::array< std::int64_t, N > &strides)
Export a Kokkos View to a Python array without copying, with an explicit logical shape and element-un...
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
The payload of a zero-copy capsule: a reference to the exported View, registered as a Releasable so t...
void release() noexcept override