core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
kokkos_teardown.hpp
Go to the documentation of this file.
1// core — uniform Kokkos teardown for every peclet Python extension module (nanobind).
2//
3// THE PROBLEM. Kokkos::finalize() has to run from a Python `atexit` hook: the alternative —
4// Kokkos's own static destructors, which run after the CUDA runtime has unloaded — aborts every
5// CUDA process at exit with cudaErrorCudartUnloading. But Python runs atexit hooks BEFORE it tears
6// down module globals, so any bound object (a Solver / Simulation / Tessellation at script or
7// notebook scope) or any zero-copy array (a capsule holding a View) that is still referenced when
8// the hook runs destroys its Kokkos View AFTER finalize, and
9// Kokkos::Impl::SharedAllocationRecord::decrement calls Kokkos::abort — `Kokkos allocation "x" is
10// being deallocated after Kokkos::finalize was called` + a backtrace, SIGABRT / exit 134 — on
11// OpenMP exactly as on CUDA. That is the normal situation in a plain script, in `python -c`, and in
12// every Jupyter / Quarto kernel; `del obj; gc.collect()` before exit was the only way out, and it
13// does not exist for a notebook.
14//
15// THE PATTERN (the same in flow, dem, voro, pnm, coupling and core.amr):
16// * every binding-side owner of Kokkos Views derives from Releasable: its constructor registers
17// it in this module's live registry, its destructor unregisters it, release() drops its Views.
18// A wrapper that cannot drop its state piecemeal releases by destroying its own bound instance
19// (destruct_bound_instance): nanobind runs the C++ destructor now and marks the Python object
20// uninitialized, so a later method call raises a TypeError instead of touching freed memory,
21// and Python's final dealloc skips the destructor.
22// * every zero-copy capsule made by view_to_ndarray (ndarray_interop.hpp) is a Releasable too: it
23// keeps a reference to the exported View and release() drops it. The array's memory is gone
24// from then on — after shutdown it is only ever touched by Python's own dealloc, never read.
25// * a C++ class that keeps its own registry (dem's Simulation::releaseAll) plugs in via add_hook.
26// * install(m) initializes Kokkos — bounding the host thread pool by the process's real CPU
27// budget when a cgroup quota makes it narrower than the visible machine (cpu_budget.hpp)
28// — registers ONE atexit hook per module that runs release_all()
29// and THEN Kokkos::finalize(), exposes it as m.finalize() for deterministic teardown
30// (idempotent; after it, every bound object and zero-copy array of the module is dead), and
31// publishes m.execution_space.
32// Each extension module statically links its own Kokkos (libkokkoscore.a), so the registry, the
33// hook and the finalize are per module: modules tear down independently, in whatever order.
34//
35// Included by binding translation units only (they link nanobind + Kokkos); never by kernels.
36#ifndef PECLET_CORE_PYTHON_KOKKOS_TEARDOWN_HPP
37#define PECLET_CORE_PYTHON_KOKKOS_TEARDOWN_HPP
38
39#include <nanobind/nanobind.h>
40
41#include <functional>
42#include <Kokkos_Core.hpp>
43#include <set>
44#include <type_traits>
45#include <vector>
46
48
50
51namespace nb = nanobind;
52
57 public:
58 Releasable() noexcept { registry().insert(this); }
59 Releasable(const Releasable&) noexcept { registry().insert(this); }
60 Releasable(Releasable&&) noexcept { registry().insert(this); }
61 Releasable& operator=(const Releasable&) noexcept { return *this; }
63 virtual ~Releasable() { registry().erase(this); }
64 virtual void release() noexcept = 0;
65
69 static auto* s = new std::set<Releasable*>;
70 return *s;
71 }
72};
73
75inline std::vector<std::function<void()>>& release_hooks() {
76 static auto* v = new std::vector<std::function<void()>>;
77 return *v;
78}
79inline void add_release_hook(std::function<void()> f) {
80 release_hooks().push_back(std::move(f));
81}
82
85inline void release_all() noexcept {
86 auto& reg = Releasable::registry();
87 std::vector<Releasable*> snapshot(reg.begin(), reg.end());
88 for (Releasable* r : snapshot)
89 if (reg.count(r))
90 r->release();
91 for (auto& h : release_hooks())
92 h();
93}
94
97inline void shutdown() noexcept {
99 if (Kokkos::is_initialized() && !Kokkos::is_finalized())
100 Kokkos::finalize();
101}
102
107template <class T>
108inline void destruct_bound_instance(T* self) noexcept {
109 nb::handle h = nb::find(self);
110 if (h.is_valid())
111 nb::inst_destruct(h);
112}
113
116inline void install(nb::module_& m) {
117 if (!Kokkos::is_initialized()) {
118 // Size the host backend by the CPUs this process may USE, not the ones it can see: inside a
119 // cgroup CPU quota (Colab, Binder, Docker, Slurm) those differ, and the oversubscribed pool
120 // spin-waits itself to a standstill — >35x, silent, on a user's first run. defaultHostThreads()
121 // returns 0, and so says nothing at all, whenever OMP_NUM_THREADS / KOKKOS_NUM_THREADS is set
122 // or the budget is the whole machine, which makes this inert on an ordinary workstation.
123 // See peclet/core/common/cpu_budget.hpp and suite docs/SCALING_ISSUES.md issue 7.
124 // Only the OpenMP backend sizes itself: its runtime reads OMP_NUM_THREADS (Kokkos reads only
125 // KOKKOS_NUM_THREADS) and its default is the whole machine. Kokkos::Threads -- what a Windows
126 // or macOS wheel carries -- does neither: without hwloc it defaults to ONE thread. So on that
127 // backend defaultHostThreads() is the difference between a multicore wheel and a 7x cut.
128 Kokkos::InitializationSettings settings;
129#if defined(KOKKOS_ENABLE_OPENMP)
130 constexpr bool kHostSizesItself =
131 std::is_same_v<Kokkos::DefaultHostExecutionSpace, Kokkos::OpenMP>;
132#else
133 constexpr bool kHostSizesItself = false;
134#endif
136 settings.set_num_threads(threads);
137 Kokkos::initialize(settings);
138 }
139 m.def("finalize", &shutdown,
140 "Release every live object and zero-copy array of this module, then Kokkos::finalize() "
141 "(deterministic teardown; also run automatically at interpreter exit). Idempotent. After "
142 "it, the module's solver objects and *_view arrays must not be used.");
143 m.attr("execution_space") = nb::str(Kokkos::DefaultExecutionSpace::name());
144 nb::module_::import_("atexit").attr("register")(nb::cpp_function(&shutdown));
145}
146
147} // namespace peclet::core::python
148
149#endif // PECLET_CORE_PYTHON_KOKKOS_TEARDOWN_HPP
Base of everything on the binding side that owns Kokkos Views on Python's behalf.
Releasable & operator=(const Releasable &) noexcept
Releasable(Releasable &&) noexcept
Releasable(const Releasable &) noexcept
static std::set< Releasable * > & registry()
This module's live registry.
Releasable & operator=(Releasable &&) noexcept
virtual void release() noexcept=0
void shutdown() noexcept
release_all() and then Kokkos::finalize().
void add_release_hook(std::function< void()> f)
void destruct_bound_instance(T *self) noexcept
Release a bound wrapper by destroying its C++ instance in place (for types whose Kokkos state cannot ...
void install(nb::module_ &m)
Module setup: initialize Kokkos (once per module — each module carries its own static Kokkos),...
void release_all() noexcept
Drop every Kokkos View owned on Python's behalf in this module (registry + hooks).
std::vector< std::function< void()> > & release_hooks()
Extra release callbacks (e.g. a C++ class's own releaseAll), run after the registry.
int defaultHostThreads(bool hostBackendSizesItself)
The host thread count to hand a backend that has not been told one, or 0 for "say nothing".
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26