ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
MpiCallbacks.hpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2026 The ESPResSo project
3 * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010
4 * Max-Planck-Institute for Polymer Research, Theory Group
5 *
6 * This file is part of ESPResSo.
7 *
8 * ESPResSo is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * ESPResSo is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22#pragma once
23
24/**
25 * @file
26 *
27 * @ref Communication::MpiCallbacks manages MPI communication using a
28 * visitor pattern. The program runs on the head node and is responsible
29 * for calling callback functions on the worker nodes when necessary,
30 * e.g. to broadcast global variables or run an algorithm in parallel.
31 *
32 * Callbacks are registered on the head node as function pointers via
33 * the @ref REGISTER_CALLBACK. The visitor pattern allows using arbitrary
34 * function signatures.
35 */
36
38
39#include <boost/mpi/collectives/broadcast.hpp>
40#include <boost/mpi/communicator.hpp>
41#include <boost/mpi/environment.hpp>
42#include <boost/mpi/packed_iarchive.hpp>
43
44#include <cassert>
45#include <memory>
46#include <tuple>
47#include <type_traits>
48#include <utility>
49#include <vector>
50
51namespace Communication {
52
53namespace detail {
54/**
55 * @brief Check if a type can be used as a callback argument.
56 *
57 * This checks is a type can be a parameter type for a MPI callback.
58 * Not allowed are pointers and non-const references, as output
59 * parameters can not work across ranks.
60 */
61template <class T>
62using is_allowed_argument =
63 std::integral_constant<bool,
64 not(std::is_pointer_v<T> ||
65 (!std::is_const_v<std::remove_reference_t<T>> &&
66 std::is_lvalue_reference_v<T>))>;
67
68/**
69 * @brief Invoke a callable with arguments from an mpi buffer.
70 *
71 * @tparam F A Callable that can be called with Args as parameters.
72 * @tparam Args Pack of arguments for @p F
73 *
74 * @param f Functor to be called
75 * @param ia Buffer to extract the parameters from
76 *
77 * @return Return value of calling @p f.
78 */
79template <class F, class... Args>
80auto invoke(F f, boost::mpi::packed_iarchive &ia) {
81 static_assert(std::conjunction_v<is_allowed_argument<Args>...>,
82 "Pointers and non-const references are not allowed as "
83 "arguments for callbacks.");
84
85 /* This is the local receive buffer for the parameters. We have to strip
86 away const so we can actually deserialize into it. */
87 std::tuple<std::remove_const_t<std::remove_reference_t<Args>>...> params;
88 std::apply([&ia](auto &&...e) { ((ia >> e), ...); }, params);
89
90 /* We add const here, so that parameters can only be by value
91 or const reference. Output parameters on callbacks are not
92 sensible because the changes are not propagated back, so
93 we make sure this does not compile. */
94 return std::apply(f, std::as_const(params));
95}
96
97/**
98 * @brief Type-erased interface for callbacks.
99 *
100 * This encapsulates the signature of the callback
101 * and the parameter transfer, so that it can be
102 * called without any type information on the parameters.
103 */
104struct callback_concept_t {
105 /**
106 * @brief Execute the callback.
107 *
108 * Unpack parameters for this callback, and then call it.
109 */
110 virtual void operator()(boost::mpi::communicator const &,
111 boost::mpi::packed_iarchive &) const = 0;
112 virtual ~callback_concept_t() = default;
113};
114
115/**
116 * @brief Callback without a return value.
117 *
118 * This is an implementation of a callback for a specific callable
119 * @p F and a set of arguments to call it with.
120 */
121template <class F, class... Args>
122struct callback_void_t final : public callback_concept_t {
123 F m_f;
124
125 callback_void_t(callback_void_t const &) = delete;
126 callback_void_t(callback_void_t &&) = delete;
127
128 template <class FRef>
129 explicit callback_void_t(FRef &&f) : m_f(std::forward<FRef>(f)) {}
130 void operator()(boost::mpi::communicator const &,
131 boost::mpi::packed_iarchive &ia) const override {
132 detail::invoke<F, Args...>(m_f, ia);
133 }
134};
135
136/** @brief Type traits for a functor. */
137template <class T> struct FunctorTypes;
138
139/** @brief Type traits for an immutable lambda. */
140template <class Class, class Ret, class... Args>
141struct FunctorTypes<Ret (Class::*)(Args...) const> {
142 using functor_type = Class;
143 using return_type = Ret;
144 using argument_types = std::tuple<Args...>;
145};
146
147template <class Class, class Ret, class... Args>
148using functor_types_from_args = FunctorTypes<Ret (Class::*)(Args...) const>;
149
150template <class F>
151using functor_types_from_lambda =
152 FunctorTypes<decltype(&std::remove_reference_t<F>::operator())>;
153
154template <class F, class C, class R, class... Args>
155auto make_model_impl(F &&f, functor_types_from_args<C, R, Args...>) {
156 return std::make_unique<callback_void_t<C, Args...>>(std::forward<F>(f));
157}
158
159/**
160 * @brief Make a @ref callback_model_t for a functor or lambda.
161 *
162 * The signature is deduced from F::operator() const, which has
163 * to exist and can not be overloaded.
164 */
165template <typename F> auto make_model(F &&f) {
166 return make_model_impl(std::forward<F>(f), functor_types_from_lambda<F>{});
167}
168
169/**
170 * @brief Make a @ref callback_model_t for a function pointer.
171 */
172template <class... Args> auto make_model(void (*f_ptr)(Args...)) {
173 return std::make_unique<callback_void_t<void (*)(Args...), Args...>>(f_ptr);
174}
175} // namespace detail
176
177/**
178 * @brief The interface of the MPI callback mechanism.
179 */
181public:
182 /**
183 * @brief RAII handle for a callback.
184 *
185 * This is what the client gets for registering a
186 * dynamic (= not function pointer) callback.
187 * It manages the lifetime of the callback handle
188 * needed to call it. The handle has a type derived
189 * from the signature of the callback, which makes
190 * it possible to do static type checking on the
191 * arguments.
192 */
193 template <class... Args> class CallbackHandle {
194 public:
195 template <typename F>
196 requires std::is_same_v<typename detail::functor_types_from_lambda<
197 F>::argument_types,
198 std::tuple<Args...>>
199 CallbackHandle(std::shared_ptr<MpiCallbacks> cb, F &&f)
200 : m_id(cb->add(std::forward<F>(f))), m_cb(std::move(cb)) {}
201
203 CallbackHandle(CallbackHandle &&rhs) noexcept = default;
206
207 private:
208 int m_id;
209 std::shared_ptr<MpiCallbacks> m_cb;
210
211 public:
212 /**
213 * @brief Call the callback managed by this handle.
214 *
215 * The arguments are passed to the remote callees, it
216 * must be possible to call the function with the provided
217 * arguments, otherwise this will not compile.
218 */
219 template <class... ArgRef>
220 auto operator()(ArgRef &&...args) const
221 /* Enable if a hypothetical function with signature void(Args..)
222 * could be called with the provided arguments. */
223 requires std::is_invocable_r_v<void, void (*)(Args...), ArgRef &&...>
224 {
225 if (m_cb)
226 m_cb->call(m_id, std::forward<ArgRef>(args)...);
227 }
228
230 if (m_cb)
231 m_cb->remove(m_id);
232 }
233
234 int id() const { return m_id; }
235 };
236
237 /* Avoid accidental copy, leads to mpi deadlock or split brain */
238 MpiCallbacks(MpiCallbacks const &) = delete;
240
241private:
242 static auto &static_callbacks() {
243 static std::vector<
244 std::pair<void (*)(), std::unique_ptr<detail::callback_concept_t>>>
245 callbacks;
246
247 return callbacks;
248 }
249
250public:
251 MpiCallbacks(boost::mpi::communicator comm,
252 std::shared_ptr<boost::mpi::environment> mpi_env)
253 : m_comm(std::move(comm)), m_mpi_env(std::move(mpi_env)) {
254 /* Add a dummy at id 0 for loop abort. */
255 m_callback_map.add(nullptr);
256
257 for (auto &[fp, handle] : static_callbacks()) {
258 m_func_ptr_to_id[fp] = m_callback_map.add(handle.get());
259 }
260 }
261
263 /* Release the clients on exit */
264 if (m_comm.rank() == 0) {
265 try {
266 abort_loop();
267 } catch (...) { // NOLINT(bugprone-empty-catch)
268 }
269 }
270 /* MPI_Finalize is unsafe if there are pending non-blocking operations */
271 m_comm.barrier();
272 m_mpi_env.reset();
273 }
274
275private:
276 /**
277 * @brief Add a new callback.
278 *
279 * Add a new callback to the system. This is a collective
280 * function that must be run on all nodes.
281 *
282 * @tparam F An object with a const call operator.
283 *
284 * @param f The callback function to add.
285 * @return A handle with which the callback can be called.
286 */
287 template <typename F> auto add(F &&f) {
288 m_callbacks.emplace_back(detail::make_model(std::forward<F>(f)));
289 return m_callback_map.add(m_callbacks.back().get());
290 }
291
292public:
293 /**
294 * @brief Add a new callback.
295 *
296 * Add a new callback to the system. This is a collective
297 * function that must be run on all nodes.
298 *
299 * @param fp Pointer to the static callback function to add.
300 */
301 template <class... Args> void add(void (*fp)(Args...)) {
302 m_callbacks.emplace_back(detail::make_model(fp));
303 const int id = m_callback_map.add(m_callbacks.back().get());
304 m_func_ptr_to_id[reinterpret_cast<void (*)()>(fp)] = id;
305 }
306
307 /**
308 * @brief Add a new callback.
309 *
310 * Add a new callback to the system. This is a collective
311 * function that must be run on all nodes.
312 *
313 * @param fp Pointer to the static callback function to add.
314 */
315 template <class... Args> static void add_static(void (*fp)(Args...)) {
316 static_callbacks().emplace_back(reinterpret_cast<void (*)()>(fp),
317 detail::make_model(fp));
318 }
319
320private:
321 /**
322 * @brief Remove callback.
323 *
324 * Remove the callback id from the callback list.
325 * This is a collective call that must be run on all nodes.
326 *
327 * @param id Identifier of the callback to remove.
328 */
329 void remove(int id) {
330 std::erase_if(m_callbacks, [ptr = m_callback_map[id]](auto const &e) {
331 return e.get() == ptr;
332 });
333 m_callback_map.remove(id);
334 }
335
336private:
337 /**
338 * @brief call a callback.
339 *
340 * Call the callback id.
341 * The method can only be called on the head node
342 * and has the prerequisite that the other nodes are
343 * in the MPI loop.
344 *
345 * @param id The callback to call.
346 * @param args Arguments for the callback.
347 */
348 template <class... Args> void call(int id, Args &&...args) const {
349 if (m_comm.rank() != 0) {
350 throw std::logic_error("Callbacks can only be invoked on rank 0.");
351 }
352
353 assert(m_callback_map.find(id) != m_callback_map.end() &&
354 "m_callback_map and m_func_ptr_to_id disagree");
355
356 /* Send request to worker nodes */
357 boost::mpi::packed_oarchive oa(m_comm);
358 oa << id;
359
360 /* Pack the arguments into a packed mpi buffer. */
361 std::apply([&oa](auto &&...e) { ((oa << e), ...); },
362 std::forward_as_tuple(std::forward<Args>(args)...));
363
364 boost::mpi::broadcast(m_comm, oa, 0);
365 }
366
367public:
368 /**
369 * @brief Call a callback on worker nodes.
370 *
371 * The callback is **not** called on the head node.
372 *
373 * This method can only be called on the head node.
374 *
375 * @param fp Pointer to the function to call.
376 * @param args Arguments for the callback.
377 */
378 template <class... Args, class... ArgRef>
379 auto call(void (*fp)(Args...), ArgRef &&...args) const
380 /* enable only if fp can be called with the provided arguments */
382 {
383 const int id = m_func_ptr_to_id.at(reinterpret_cast<void (*)()>(fp));
384
385 call(id, std::forward<ArgRef>(args)...);
386 }
387
388 /**
389 * @brief Call a callback on all nodes.
390 *
391 * This calls a callback on all nodes, including the head node.
392 *
393 * This method can only be called on the head node.
394 *
395 * @param fp Pointer to the function to call.
396 * @param args Arguments for the callback.
397 */
398 template <class... Args, class... ArgRef>
399 auto call_all(void (*fp)(Args...), ArgRef &&...args) const
400 /* enable only if fp can be called with the provided arguments */
402 {
403 call(fp, args...);
404 fp(args...);
405 }
406
407 /**
408 * @brief Start the MPI loop.
409 *
410 * This is the callback loop for the worker nodes. They block
411 * on the MPI call and wait for a new callback request
412 * coming from the head node.
413 * This should be run on the worker nodes and must be running
414 * so that the head node can issue call().
415 */
416 void loop() const {
417 for (;;) {
418 /* Communicate callback id and parameters */
419 boost::mpi::packed_iarchive ia(m_comm);
420 boost::mpi::broadcast(m_comm, ia, 0);
421
422 int request;
423 ia >> request;
424
425 if (request == LOOP_ABORT) {
426 break;
427 }
428 /* Call the callback */
429 m_callback_map[request]->operator()(m_comm, ia);
430 }
431 }
432
433 /**
434 * @brief Abort the MPI loop.
435 *
436 * Make the worker nodes exit the MPI loop.
437 */
438 void abort_loop() { call(LOOP_ABORT); }
439
440 /**
441 * @brief The boost mpi communicator used by this instance
442 */
443 boost::mpi::communicator const &comm() const { return m_comm; }
444
445private:
446 /**
447 * @brief Id for the @ref abort_loop. Has to be 0.
448 */
449 static constexpr int LOOP_ABORT = 0;
450
451 /**
452 * The MPI communicator used for the callbacks.
453 */
454 boost::mpi::communicator m_comm;
455
456 /**
457 * The MPI environment used for the callbacks.
458 */
459 std::shared_ptr<boost::mpi::environment> m_mpi_env;
460
461 /**
462 * Internal storage for the callback functions.
463 */
464 std::vector<std::unique_ptr<detail::callback_concept_t>> m_callbacks;
465
466 /**
467 * Map of ids to callbacks.
468 */
470
471 /**
472 * Mapping of function pointers to ids, so static callbacks can be
473 * called by their pointer.
474 */
475 std::unordered_map<void (*)(), int> m_func_ptr_to_id;
476};
477
478template <class... Args>
480
481/**
482 * @brief Helper class to add callbacks before main.
483 *
484 * Should not be used directly, but via @ref REGISTER_CALLBACK.
485 */
487
488public:
490
491 template <class... Args> explicit RegisterCallback(void (*cb)(Args...)) {
493 }
494};
495} /* namespace Communication */
496
497/**
498 * @brief Register a static callback without return value.
499 *
500 * This registers a function as an mpi callback.
501 * The macro should be used at global scope.
502 *
503 * @param cb A function
504 */
505#define REGISTER_CALLBACK(cb) \
506 namespace Communication { \
507 static ::Communication::RegisterCallback register_##cb(&(cb)); \
508 }
Keep an enumerated list of T objects, managed by the class.
CallbackHandle(std::shared_ptr< MpiCallbacks > cb, F &&f)
CallbackHandle(CallbackHandle &&rhs) noexcept=default
auto operator()(ArgRef &&...args) const
Call the callback managed by this handle.
CallbackHandle(CallbackHandle const &)=delete
CallbackHandle & operator=(CallbackHandle &&rhs) noexcept=default
CallbackHandle & operator=(CallbackHandle const &)=delete
The interface of the MPI callback mechanism.
auto call_all(void(*fp)(Args...), ArgRef &&...args) const
Call a callback on all nodes.
MpiCallbacks(boost::mpi::communicator comm, std::shared_ptr< boost::mpi::environment > mpi_env)
void add(void(*fp)(Args...))
Add a new callback.
boost::mpi::communicator const & comm() const
The boost mpi communicator used by this instance.
void abort_loop()
Abort the MPI loop.
static void add_static(void(*fp)(Args...))
Add a new callback.
MpiCallbacks & operator=(MpiCallbacks const &)=delete
auto call(void(*fp)(Args...), ArgRef &&...args) const
Call a callback on worker nodes.
MpiCallbacks(MpiCallbacks const &)=delete
void loop() const
Start the MPI loop.
Helper class to add callbacks before main.
RegisterCallback(void(*cb)(Args...))
Container for objects that are identified by a numeric id.
cudaStream_t stream[1]
CUDA streams for parallel computing on CPU and GPU.
STL namespace.