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 m_skip_worker_nodes = m_comm.size() == 1;
261 }
262
264 /* Release the clients on exit */
265 if (m_comm.rank() == 0) {
266 try {
267 abort_loop();
268 } catch (...) { // NOLINT(bugprone-empty-catch)
269 }
270 }
271 /* MPI_Finalize is unsafe if there are pending non-blocking operations */
272 m_comm.barrier();
273 m_mpi_env.reset();
274 }
275
276private:
277 /**
278 * @brief Add a new callback.
279 *
280 * Add a new callback to the system. This is a collective
281 * function that must be run on all nodes.
282 *
283 * @tparam F An object with a const call operator.
284 *
285 * @param f The callback function to add.
286 * @return A handle with which the callback can be called.
287 */
288 template <typename F> auto add(F &&f) {
289 m_callbacks.emplace_back(detail::make_model(std::forward<F>(f)));
290 return m_callback_map.add(m_callbacks.back().get());
291 }
292
293public:
294 /**
295 * @brief Add a new callback.
296 *
297 * Add a new callback to the system. This is a collective
298 * function that must be run on all nodes.
299 *
300 * @param fp Pointer to the static callback function to add.
301 */
302 template <class... Args> void add(void (*fp)(Args...)) {
303 m_callbacks.emplace_back(detail::make_model(fp));
304 const int id = m_callback_map.add(m_callbacks.back().get());
305 m_func_ptr_to_id[reinterpret_cast<void (*)()>(fp)] = id;
306 }
307
308 /**
309 * @brief Add a new callback.
310 *
311 * Add a new callback to the system. This is a collective
312 * function that must be run on all nodes.
313 *
314 * @param fp Pointer to the static callback function to add.
315 */
316 template <class... Args> static void add_static(void (*fp)(Args...)) {
317 static_callbacks().emplace_back(reinterpret_cast<void (*)()>(fp),
318 detail::make_model(fp));
319 }
320
321private:
322 /**
323 * @brief Remove callback.
324 *
325 * Remove the callback id from the callback list.
326 * This is a collective call that must be run on all nodes.
327 *
328 * @param id Identifier of the callback to remove.
329 */
330 void remove(int id) {
331 std::erase_if(m_callbacks, [ptr = m_callback_map[id]](auto const &e) {
332 return e.get() == ptr;
333 });
334 m_callback_map.remove(id);
335 }
336
337private:
338 /**
339 * @brief call a callback.
340 *
341 * Call the callback id.
342 * The method can only be called on the head node
343 * and has the prerequisite that the other nodes are
344 * in the MPI loop.
345 *
346 * @param id The callback to call.
347 * @param args Arguments for the callback.
348 */
349 template <class... Args> void call(int id, Args &&...args) const {
350 if (m_comm.rank() != 0) {
351 throw std::logic_error("Callbacks can only be invoked on rank 0.");
352 }
353
354 assert(m_callback_map.find(id) != m_callback_map.end() &&
355 "m_callback_map and m_func_ptr_to_id disagree");
356
357 /* Send request to worker nodes */
358 boost::mpi::packed_oarchive oa(m_comm);
359 oa << id;
360
361 /* Pack the arguments into a packed mpi buffer. */
362 std::apply([&oa](auto &&...e) { ((oa << e), ...); },
363 std::forward_as_tuple(std::forward<Args>(args)...));
364
365 boost::mpi::broadcast(m_comm, oa, 0);
366 }
367
368public:
369 /**
370 * @brief Call a callback on worker nodes.
371 *
372 * The callback is **not** called on the head node.
373 *
374 * This method can only be called on the head node.
375 *
376 * @param fp Pointer to the function to call.
377 * @param args Arguments for the callback.
378 */
379 template <class... Args, class... ArgRef>
380 auto call(void (*fp)(Args...), ArgRef &&...args) const
381 /* enable only if fp can be called with the provided arguments */
383 {
384 if (m_skip_worker_nodes) {
385 return;
386 }
387 const int id = m_func_ptr_to_id.at(reinterpret_cast<void (*)()>(fp));
388
389 call(id, std::forward<ArgRef>(args)...);
390 }
391
392 /**
393 * @brief Call a callback on all nodes.
394 *
395 * This calls a callback on all nodes, including the head node.
396 *
397 * This method can only be called on the head node.
398 *
399 * @param fp Pointer to the function to call.
400 * @param args Arguments for the callback.
401 */
402 template <class... Args, class... ArgRef>
403 auto call_all(void (*fp)(Args...), ArgRef &&...args) const
404 /* enable only if fp can be called with the provided arguments */
406 {
407 call(fp, args...);
408 fp(args...);
409 }
410
411 /**
412 * @brief Start the MPI loop.
413 *
414 * This is the callback loop for the worker nodes. They block
415 * on the MPI call and wait for a new callback request
416 * coming from the head node.
417 * This should be run on the worker nodes and must be running
418 * so that the head node can issue call().
419 */
420 void loop() const {
421 assert(m_comm.rank() != 0);
422 for (;;) {
423 /* Communicate callback id and parameters */
424 boost::mpi::packed_iarchive ia(m_comm);
425 boost::mpi::broadcast(m_comm, ia, 0);
426
427 int request;
428 ia >> request;
429
430 if (request == LOOP_ABORT) {
431 break;
432 }
433 /* Call the callback */
434 m_callback_map[request]->operator()(m_comm, ia);
435 }
436 }
437
438 /**
439 * @brief Abort the MPI loop.
440 *
441 * Make the worker nodes exit the MPI loop.
442 */
443 void abort_loop() { call(LOOP_ABORT); }
444
445 /**
446 * @brief The boost mpi communicator used by this instance
447 */
448 boost::mpi::communicator const &comm() const { return m_comm; }
449
450private:
451 /**
452 * @brief Id for the @ref abort_loop. Has to be 0.
453 */
454 static constexpr int LOOP_ABORT = 0;
455
456 /**
457 * The MPI communicator used for the callbacks.
458 */
459 boost::mpi::communicator m_comm;
460
461 /**
462 * The MPI environment used for the callbacks.
463 */
464 std::shared_ptr<boost::mpi::environment> m_mpi_env;
465
466 /**
467 * Internal storage for the callback functions.
468 */
469 std::vector<std::unique_ptr<detail::callback_concept_t>> m_callbacks;
470
471 /**
472 * Map of ids to callbacks.
473 */
475
476 /**
477 * Mapping of function pointers to ids, so static callbacks can be
478 * called by their pointer.
479 */
480 std::unordered_map<void (*)(), int> m_func_ptr_to_id;
481
482 /** Workers dispatch can be skipped when world size is 1. */
483 bool m_skip_worker_nodes;
484};
485
486template <class... Args>
488
489/**
490 * @brief Helper class to add callbacks before main.
491 *
492 * Should not be used directly, but via @ref REGISTER_CALLBACK.
493 */
495
496public:
498
499 template <class... Args> explicit RegisterCallback(void (*cb)(Args...)) {
501 }
502};
503} /* namespace Communication */
504
505/**
506 * @brief Register a static callback without return value.
507 *
508 * This registers a function as an mpi callback.
509 * The macro should be used at global scope.
510 *
511 * @param cb A function
512 */
513#define REGISTER_CALLBACK(cb) \
514 namespace Communication { \
515 static ::Communication::RegisterCallback register_##cb(&(cb)); \
516 }
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.