include/pops/core/foundation/allocator.hpp Source FileΒΆ

adc_cpp: include/pops/core/foundation/allocator.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
allocator.hpp
Go to the documentation of this file.
1
16
17#pragma once
18
19#include <cstddef>
20#include <memory>
21
22namespace pops {
23
26struct ArenaStats {
27 long hits = 0; // allocations served by the pool (no kokkos_malloc)
28 long misses = 0; // allocations that triggered a kokkos_malloc<SharedSpace>
29 long fences = 0; // batched Kokkos::fence barriers (recycling pending blocks)
30 std::size_t reserved_bytes = 0; // total managed memory held by the pool
31};
32} // namespace pops
33
34#if defined(POPS_HAS_KOKKOS)
35#include <pops/core/foundation/kokkos_env.hpp> // detail::ensure_kokkos_initialized: Kokkos init BEFORE kokkos_malloc
36#include <Kokkos_Core.hpp>
37
38#include <mutex>
39#include <new>
40#include <unordered_map>
41#include <vector>
42
43namespace pops {
44
45static_assert(
46 Kokkos::has_shared_space,
47 "pops: the Kokkos backend must provide SharedSpace (unified memory) for the device "
48 "Fab; enable a Cuda/HIP/SYCL backend (or a host backend, where SharedSpace is HostSpace)");
49
50// Cache of unified-memory allocations (Kokkos::SharedSpace), free-list by size (bytes).
51//
52// Async SAFETY: a kernel may still read/write a Fab at the moment of its destruction. Previously we
53// relied on the implicit synchronization of cudaFree; here we reproduce that barrier but BATCHED and
54// PORTABLE (Kokkos::fence): a freed block goes into `pending_` (not yet reusable); when an
55// allocation lacks a ready block, a single Kokkos::fence() drains the device and moves ALL
56// pending blocks to `ready_`. A block from `ready_` has therefore necessarily had its last device
57// use finished before the host (value-init of the vector) overwrites it.
70class ManagedArena {
71 public:
72 // PROCESS-LIFETIME singleton, NEVER destroyed (intentional leak of an object and of
73 // its tables -- the OS reclaims everything at exit). WHY not an ordinary local static (real bug,
74 // issue #271, gdb on CI glibc): instance() is inline and the pybind11 module _pops is
75 // compiled with HIDDEN visibility -> each DSL .so loader (dlopen, never unloaded) has ITS
76 // own copy of the static (verified with LD_DEBUG=bindings: all ManagedArena symbols of the .so
77 // bind to the .so itself). At exit, the destructors of these copies (registered LATE, hence
78 // run EARLY, LIFO) destroyed the tables BEFORE the module's atexit Kokkos::finalize
79 // (registered early, run late), whose finalize hooks called release_all() back on
80 // DESTROYED arenas -> frees of garbage pointers -> "free(): corrupted unsorted chunks" /
81 // SIGSEGV at teardown. With the never-destroyed singleton, the instance stays valid at ANY moment
82 // of the process shutdown: no longer any dependency on the order of exit handlers. The pool
83 // blocks are still returned to Kokkos by release_all (finalize hook); only the TABLES (maps of
84 // pointers) are leaked, by construction.
85 static ManagedArena& instance() {
86 static ManagedArena* a = new ManagedArena();
87 return *a;
88 }
89
90 void* allocate(std::size_t bytes) {
91 if (bytes == 0)
92 return nullptr;
93 // CRUCIAL: a Fab can be constructed BEFORE any for_each (hence before the lazy init on the
94 // kernel side). kokkos_malloc requires Kokkos initialized -> we guarantee the init HERE too.
95 // Without this, the Kokkos build crashes at the very first allocation (regression identified on
96 // build-kokkos). Outside the lock.
97 detail::ensure_kokkos_initialized();
98 std::lock_guard<std::mutex> lk(m_);
99 std::call_once(hook_once_,
100 [] { // return the blocks at Kokkos::finalize (otherwise "leaked" allocation)
101 Kokkos::push_finalize_hook([] { ManagedArena::instance().release_all(); });
102 });
103 if (void* p = pop_ready(bytes))
104 return p;
105 if (pending_count_ > 0) {
106 Kokkos::
107 fence(); // batched, portable barrier (drains in-flight kernels; former cudaDeviceSynchronize)
108 ++fences_;
109 for (auto& kv : pending_) {
110 auto& r = ready_[kv.first];
111 r.insert(r.end(), kv.second.begin(), kv.second.end());
112 kv.second.clear();
113 }
114 pending_count_ = 0;
115 if (void* p = pop_ready(bytes))
116 return p;
117 }
118 void* p =
119 Kokkos::kokkos_malloc<Kokkos::SharedSpace>("pops_fab", bytes); // portable unified memory
120 if (!p)
121 throw std::bad_alloc();
122 ++misses_;
123 reserved_ += bytes;
124 return p;
125 }
126
127 // Freed block: pending (not reusable before the next batched barrier). No
128 // immediate kokkos_free (the pool lives until the end of the process; release_all returns everything at finalize).
129 void deallocate(void* p, std::size_t bytes) {
130 if (!p)
131 return;
132 std::lock_guard<std::mutex> lk(m_);
133 pending_[bytes].push_back(p);
134 ++pending_count_;
135 }
136
137 // Kokkos::finalize hook: frees all blocks (ready + pending) via kokkos_free BEFORE the memory
138 // spaces shut down, so as to leave no unreturned Kokkos allocation (the pool never frees
139 // along the way). Called only once, outside any concurrent allocation.
140 void release_all() {
141 std::lock_guard<std::mutex> lk(m_);
142 for (auto& kv : ready_)
143 for (void* p : kv.second)
144 Kokkos::kokkos_free<Kokkos::SharedSpace>(p);
145 for (auto& kv : pending_)
146 for (void* p : kv.second)
147 Kokkos::kokkos_free<Kokkos::SharedSpace>(p);
148 ready_.clear();
149 pending_.clear();
150 pending_count_ = 0;
151 }
152
153 ArenaStats stats() {
154 std::lock_guard<std::mutex> lk(m_);
155 return ArenaStats{hits_, misses_, fences_, reserved_};
156 }
157
158 private:
159 void* pop_ready(std::size_t bytes) {
160 auto it = ready_.find(bytes);
161 if (it != ready_.end() && !it->second.empty()) {
162 void* p = it->second.back();
163 it->second.pop_back();
164 ++hits_;
165 return p;
166 }
167 return nullptr;
168 }
169
170 std::mutex m_;
171 std::once_flag hook_once_; // single registration of the Kokkos finalize hook
172 std::unordered_map<std::size_t, std::vector<void*>> ready_; // safe, reusable
173 std::unordered_map<std::size_t, std::vector<void*>> pending_; // freed, to drain
174 long pending_count_ = 0;
175 long hits_ = 0, misses_ = 0, fences_ = 0;
176 std::size_t reserved_ = 0;
177};
178
179inline ArenaStats arena_stats() {
180 return ManagedArena::instance().stats();
181}
182
186template <class T>
187struct ManagedAllocator {
188 using value_type = T;
189 ManagedAllocator() noexcept = default;
190 template <class U>
191 ManagedAllocator(const ManagedAllocator<U>&) noexcept {}
192
193 T* allocate(std::size_t n) {
194 return static_cast<T*>(ManagedArena::instance().allocate(n * sizeof(T)));
195 }
196 void deallocate(T* p, std::size_t n) noexcept {
197 ManagedArena::instance().deallocate(p, n * sizeof(T));
198 }
199};
200
201template <class A, class B>
202bool operator==(const ManagedAllocator<A>&, const ManagedAllocator<B>&) noexcept {
203 return true; // stateless: all equal (shared singleton pool)
204}
205template <class A, class B>
206bool operator!=(const ManagedAllocator<A>&, const ManagedAllocator<B>&) noexcept {
207 return false;
208}
209
210template <class T>
211using fab_allocator = ManagedAllocator<T>;
212
213// Allocator for the MPI COMMUNICATION BUFFERS (sbuf/rbuf of fill_boundary), DISTINCT from
214// fab_allocator: definitely NO managed/device memory here. A CUDA-aware MPI (e.g.
215// OpenMPI 4.1.7, PML ob1 + BTL smcuda) detects a device/managed pointer (cuPointerGetAttribute)
216// and attempts a device->device transfer via CUDA IPC (cuIpcOpenMemHandle). Under GPU cgroup
217// isolation (srun --gpus-per-task=1, each rank sees ONLY its GPU as device 0), the IPC handle exported by
218// the peer points to an invisible GPU -> the open cannot succeed -> DEADLOCK of the rendezvous.
219// So we allocate in PINNED HOST memory (Kokkos::SharedHostPinnedSpace): accessible from the device
220// (the pack/unpack for_each kernels write into it directly, like SharedSpace) BUT seen as
221// HOST memory by MPI -> normal host path, NEVER IPC, robust whatever the launch environment.
222// Kokkos host backend: SharedHostPinnedSpace == HostSpace (nothing changes). See fill_boundary.hpp.
223static_assert(Kokkos::has_shared_host_pinned_space,
224 "pops: the Kokkos backend must provide SharedHostPinnedSpace (pinned host memory) "
225 "for the MPI communication buffers of fill_boundary");
226
231template <class T>
232struct PinnedAllocator {
233 using value_type = T;
234 PinnedAllocator() noexcept = default;
235 template <class U>
236 PinnedAllocator(const PinnedAllocator<U>&) noexcept {}
237
238 T* allocate(std::size_t n) {
239 if (n == 0)
240 return nullptr;
241 detail::ensure_kokkos_initialized(); // kokkos_malloc requires Kokkos initialized
242 void* p = Kokkos::kokkos_malloc<Kokkos::SharedHostPinnedSpace>("pops_comm", n * sizeof(T));
243 if (!p)
244 throw std::bad_alloc();
245 return static_cast<T*>(p);
246 }
247 void deallocate(T* p, std::size_t) noexcept {
248 if (p)
249 Kokkos::kokkos_free<Kokkos::SharedHostPinnedSpace>(p);
250 }
251};
252template <class A, class B>
253bool operator==(const PinnedAllocator<A>&, const PinnedAllocator<B>&) noexcept {
254 return true;
255}
256template <class A, class B>
257bool operator!=(const PinnedAllocator<A>&, const PinnedAllocator<B>&) noexcept {
258 return false;
259}
260
261template <class T>
262using comm_allocator = PinnedAllocator<T>;
263
264} // namespace pops
265
266#else
267
268namespace pops {
269template <class T>
270using fab_allocator = std::allocator<T>;
271
272// Outside Kokkos: MPI buffers in ordinary host memory (CPU build unchanged, byte-identical to before).
273template <class T>
274using comm_allocator = std::allocator<T>;
275
276// Stub outside unified memory: no pool, no stats (CPU build unchanged).
278 return ArenaStats{};
279}
280} // namespace pops
281
282#endif
Shared Kokkos lifecycle: lazy init + device barrier.
Definition amr_hierarchy.hpp:29
ArenaStats arena_stats()
Definition allocator.hpp:277
std::allocator< T > comm_allocator
Definition allocator.hpp:274
std::allocator< T > fab_allocator
Definition allocator.hpp:270
ManagedArena pool statistics: hits/misses/fences and retained bytes.
Definition allocator.hpp:26
long misses
Definition allocator.hpp:28
std::size_t reserved_bytes
Definition allocator.hpp:30
long hits
Definition allocator.hpp:27
long fences
Definition allocator.hpp:29