ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
HaloExchange.cpp
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/** \file
22 * Asynchronous, split-phase ghost-communication engine.
23 */
24
26
27#ifdef ESPRESSO_CALIPER
28#include "caliper_utils.hpp"
29#endif
30
31#include "BoxGeometry.hpp"
32#include "ghosts.hpp"
33#include "ghosts/HaloPlan.hpp"
36
37#include <boost/mpi/collectives.hpp>
38#include <boost/mpi/nonblocking.hpp>
39
40#include <algorithm>
41#include <array>
42#include <cassert>
43#include <cstddef>
44#include <functional>
45#include <memory>
46#include <span>
47#include <vector>
48
49namespace GhostComm {
50
51namespace {
52
53/**
54 * @brief MPI tags for ghost communications.
55 *
56 * One tag per exchange-kind keeps concurrent exchanges (a future goal) from
57 * aliasing: within a single exchange each message is already uniquely matched
58 * by @c (peer, tag). The BONDS transfer needs its own tag because it is sent
59 * as a *second* message to the same peer, mirroring the legacy two-transfer.
60 */
61enum GhostTag : int {
63 TAG_FORCE = 101,
66 TAG_BONDS = 104,
67};
68
69int main_tag(unsigned data_parts) {
70 if (data_parts & GHOSTTRANS_PARTNUM)
71 return TAG_PARTNUM;
72 if (data_parts & GHOSTTRANS_FORCE)
73 return TAG_FORCE;
74 if (data_parts & GHOSTTRANS_POSITION)
75 return TAG_POSITION;
76 return TAG_DEFAULT;
77}
78// NOTE: the (peer, tag) pair is what makes each MPI message unambiguous.
79// This scheme is safe only when each peer appears at most once in the plan
80// (the "one NeighborComm per peer" invariant). See halo_exchange_start @pre.
81
82/** @brief View a list of cell pointers as a span for the packing routines. */
83std::span<ParticleList *const> as_span(std::vector<ParticleList *> const &v) {
84 return {v.data(), v.size()};
85}
86
87/** @brief Extract the plain cell pointers from a list of send regions. */
88std::vector<ParticleList *> region_cells(std::vector<SendRegion> const &send) {
89 std::vector<ParticleList *> cells;
90 cells.reserve(send.size());
91 for (auto const &r : send)
92 cells.emplace_back(r.cell);
93 return cells;
94}
95
96/**
97 * @brief Pack all send regions of one NeighborComm into a single buffer.
98 *
99 * All regions are packed with a **single** @c pack_cells call so that bonds
100 * produce exactly one boost binary archive per neighbor message -- matching
101 * the single @c unpack_cells call on the receive side. Concatenating
102 * per-region archives would corrupt the bond stream (the second archive
103 * header is misread as bond data by the receiver).
104 *
105 * Per-region shifts are honored via the common shift. All @c SendRegion.shift
106 * values within a @c NeighborComm are equal — @c RegularDecomposition::
107 * make_halo_plan() sets them all to @c {}. This lets us pack every region's
108 * cells in one @c pack_cells call (a single bond archive, matching the
109 * receiver's single @c unpack_cells).
110 *
111 * @note If a future decomposition (e.g. a Lees-Edwards refactor) ever stores
112 * DISTINCT per-region shifts, @c pack_regions must be generalized to pack
113 * each region's non-bond (flat) data with its own shift while still writing
114 * all bonds into ONE shared archive — do not simply concatenate per-region
115 * archives (that was the bond-corruption bug fixed in commit 314e7c366b).
116 */
117void pack_regions(CommBuf &buf, std::vector<SendRegion> const &regions,
118 BoxGeometry const &box, unsigned data_parts) {
119#if defined(ESPRESSO_ADDITIONAL_CHECKS) and not defined(NDEBUG)
120 // All regions in a NeighborComm must share the same shift. If per-region
121 // shifts ever diverge the packer must be generalized (separate archives).
122 if (not regions.empty()) {
123 auto const &ref = regions.front().shift;
124 for (auto const &r : regions) {
125 assert(r.shift == ref &&
126 "pack_regions: per-region shifts differ within one NeighborComm "
127 "-- packer must be generalized for per-region shift support");
128 }
129 }
130#endif
131
132 Utils::Vector3d const common_shift =
133 regions.empty() ? Utils::Vector3d{} : regions.front().shift;
134
135 auto const cells = region_cells(regions);
136 pack_cells(buf, as_span(cells), common_shift, box, data_parts);
137}
138
139/**
140 * @brief Collective (broadcast/reduce-sum) section.
141 *
142 * For each root rank in <tt>[0, comm.size())</tt>:
143 *
144 * 1. Broadcast (Push): root packs its owned cell (cells[root]) and broadcasts
145 * the buffer to all ranks; every non-root rank unpacks into cells[root]
146 * (their ghost copy of root's data).
147 * 2. ReduceSum (Reduce): every rank packs cells[root] (ghost-force
148 * contribution) and reduces to root with @c std::plus on the raw double buffer;
149 * root unpacks into cells[root] (the owned cell, so forces accumulate there).
150 *
151 * The legacy code uses one GHOST_BCST (or GHOST_RDCE) step per rank, each
152 * step addressing a single cell pointer (<tt>ghost_comm.part_lists[0] ==
153 * &cells.at(n)</tt>). We replicate that loop exactly.
154 */
155void run_collective(HaloPlan const &plan, BoxGeometry const &box,
156 unsigned data_parts, ExchangeOp op) {
157 // Precondition: for non-PARTNUM data parts, ghost cell sizes must already be
158 // synced by a prior GHOSTTRANS_PARTNUM exchange (same invariant as the legacy
159 // GHOST_BCST/GHOST_RDCE path). The per-root broadcast/reduce byte count is
160 // derived from the (already-sized) cells; a size mismatch across ranks would
161 // be undefined behavior.
162
163 if (!plan.collective or plan.collective->pattern == CollectivePattern::None) {
164 return;
165 }
166
167 auto const &cs = *plan.collective;
168 auto const &comm = plan.comm;
169 int const comm_size = comm.size();
170 int const my_rank = comm.rank();
171
172 // Use op.direction to select the actual MPI collective: Push -> broadcast
173 // (particles flow from owner to all ghosts); Reduce -> reduce-sum (ghost
174 // forces flow back to owner). cs.pattern == Broadcast marks the section as
175 // active; the engine caller sets op correctly for each exchange.
176 bool const is_broadcast = (op.direction == Direction::Push);
177
178 // cs.cells has one entry per rank: cs.cells[root] is the ParticleList for
179 // that root rank (owned on root, ghost on all others).
180 assert(static_cast<int>(cs.cells.size()) == comm_size);
181
182 CommBuf buf;
183 // boost::mpi's pointer-based collectives bind a reference to the buffer even
184 // for count == 0; an empty CommBuf yields data() == nullptr, which is
185 // undefined behaviour (flagged by UBSan). Substitute a dummy address for
186 // empty buffers -- the count of 0 guarantees it is never dereferenced.
187 alignas(double) static char empty_sentinel[sizeof(double)];
188 auto const safe_data = [](CommBuf &b) {
189 return b.size() != 0u ? b.data() : &empty_sentinel[0];
190 };
191
192 for (int root = 0; root < comm_size; ++root) {
193 ParticleList *cell = cs.cells[static_cast<std::size_t>(root)];
194 auto const cell_span = std::span<ParticleList *const>{&cell, 1};
195
196 if (is_broadcast) {
197 // Push: root broadcasts its owned particles to all other ranks.
198 if (my_rank == root) {
199 pack_cells(buf, cell_span, {}, box, data_parts);
200 boost::mpi::broadcast(comm, safe_data(buf),
201 static_cast<int>(buf.size()), root);
202 boost::mpi::broadcast(comm, buf.bonds(), root);
203 } else {
204 buf.resize(calc_transmit_size(cell_span, box, data_parts));
205 buf.bonds().clear();
206 boost::mpi::broadcast(comm, safe_data(buf),
207 static_cast<int>(buf.size()), root);
208 boost::mpi::broadcast(comm, buf.bonds(), root);
209 unpack_cells(buf, cell_span, box, data_parts);
210 }
211 } else {
212 // ReduceSum: every rank sends its ghost-force contribution; root
213 // accumulates into its owned cell. Mirrors the legacy GHOST_RDCE
214 // which reduces the raw double buffer with std::plus<double>.
215 pack_cells(buf, cell_span, {}, box, data_parts);
216 auto *raw = reinterpret_cast<double *>(safe_data(buf));
217 int const count = static_cast<int>(buf.size() / sizeof(double));
218 if (my_rank == root) {
219 CommBuf recv_buf;
220 recv_buf.resize(buf.size());
221 auto *recv_raw = reinterpret_cast<double *>(safe_data(recv_buf));
222 boost::mpi::reduce(comm, raw, count, recv_raw, std::plus<double>{},
223 root);
224 unpack_cells(recv_buf, cell_span, box, data_parts);
225 } else {
226 boost::mpi::reduce(comm, raw, count, std::plus<double>{}, root);
227 }
228 }
229 }
230}
231
232} // namespace
233
235 unsigned data_parts, ExchangeOp op,
236 ExchangeBuffers &bufs) {
237 GhostExchange st;
238 st.op = op;
239 st.data_parts = data_parts;
240 st.box = &box;
241 st.plan = &plan;
242 st.bufs = &bufs;
243 if (data_parts == GHOSTTRANS_NONE)
244 return st;
245
246#ifdef ESPRESSO_ADDITIONAL_CHECKS
247 // Cross-rank symmetry check: run once per plan at first use.
248 //
249 // This check is deferred from the decomposition constructors to here because:
250 // (a) During checkpoint loading, decompositions are transiently rebuilt
251 // while maximal_cutoff is rank-divergent (different cell grids per rank
252 // for a brief window before the next consistent rebuild). The
253 // transient plan is never used, so its asymmetry is harmless — but a
254 // ctor-time collective would fire and abort.
255 // (b) A collective inside a ctor risks partial-abort deadlock: if one rank
256 // throws before entering the all_to_all, the others spin forever.
257 //
258 // All ranks enter halo_exchange_start together (ghost exchange is
259 // collective), so the all_to_all inside validate_halo_plan_symmetry is safe
260 // here. plan.symmetry_validated is mutable and rearmed to false whenever the
261 // plan object is rebuilt (every decomposition change constructs a fresh
262 // HaloPlan).
263 if (!plan.symmetry_validated) {
266 "halo_exchange first use"));
267 plan.symmetry_validated = true;
268 }
269 // Peer-uniqueness is validated at plan-build time by validate_halo_plan().
270 // Op-sanity: Combine::Add is only valid for reducible parts
271 // (FORCE, FORCE|TORQUE, RATTLE). TORQUE may only appear together with
272 // FORCE (it is never sent alone).
273 // Compute the predicate outside assert() so the #ifdef is not embedded in
274 // macro arguments (non-portable under -Werror).
275 [[maybe_unused]] bool const force_only = (data_parts == GHOSTTRANS_FORCE);
276#ifdef ESPRESSO_ROTATION
277 [[maybe_unused]] bool const force_with_torque =
278 (data_parts == (GHOSTTRANS_FORCE | GHOSTTRANS_TORQUE));
279#else
280 [[maybe_unused]] bool const force_with_torque = false;
281#endif
282 [[maybe_unused]] bool reducible = (force_only or force_with_torque);
283#ifdef ESPRESSO_BOND_CONSTRAINT
284 reducible = reducible or (data_parts == GHOSTTRANS_RATTLE);
285#endif
286#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
287 reducible = reducible or (data_parts == GHOSTTRANS_DIPFLD);
288#endif
289 assert((op.combine != Combine::Add or reducible) &&
290 "Combine::Add only valid for reducible parts (FORCE, FORCE|TORQUE, "
291 "RATTLE, DIPFLD)");
292#endif // ESPRESSO_ADDITIONAL_CHECKS
293
294 auto const &comm = plan.comm;
295 auto const n = plan.neighbors.size();
296
297 // Resize the pool to the current neighbor count. Existing entries retain
298 // their vector capacity so that, after the first call (warm-up), CommBuf
299 // ::resize on a same-sized or smaller payload is a no-op on the allocator.
300 bufs.send.resize(n);
301 bufs.recv.resize(n);
302 bufs.send_cells.resize(n);
303 bufs.recv_cells.resize(n);
304 bufs.slot_to_neighbor.resize(n);
305 // Clear and reuse the request vector (capacity retained).
306 bufs.requests.clear();
307
308 bool const push = op.direction == Direction::Push;
309 bool const bonds = (data_parts & GHOSTTRANS_BONDS) != 0u;
310 int const tag = main_tag(data_parts);
311
312 // Resolve, per neighbor, which cells we send from and which we receive into.
313 // Push: send from the region cells, receive into the ghost (recv) cells.
314 // Reduce: roles swap -- send from ghost cells, receive into region cells,
315 // adding on arrival.
316 for (std::size_t i = 0; i < n; ++i) {
317 auto const &nc = plan.neighbors[i];
318 if (push) {
319 bufs.send_cells[i] = region_cells(nc.send);
320 bufs.recv_cells[i] = nc.recv;
321 } else {
322 bufs.send_cells[i] = nc.recv;
323 bufs.recv_cells[i] = region_cells(nc.send);
324 }
325 }
326
327 // 1) Pack all send buffers. Done before posting receives so that sizes are
328 // known; also isolates the serialisation work in the ghost/pack region.
329#ifdef ESPRESSO_CALIPER
331 CALI_MARK_BEGIN("ghost/pack");
332#endif
333 for (std::size_t i = 0; i < n; ++i) {
334 auto const &nc = plan.neighbors[i];
335 if (push) {
336 pack_regions(bufs.send[i], nc.send, box, data_parts);
337 } else {
338 // Reduce: pack plain ghost cells, no shift.
339 pack_cells(bufs.send[i], as_span(bufs.send_cells[i]), {}, box,
340 data_parts);
341 }
342 }
343#ifdef ESPRESSO_CALIPER
345 CALI_MARK_END("ghost/pack");
346#endif
347
348 // 2) Post ALL receives first (deadlock-free), then ALL sends. Sizes are
349 // known a-priori from the packed size of the cells we will receive into,
350 // except PARTNUM whose ghost cells are not sized yet -- there we post a
351 // fixed-size recv and let unpack_cells resize the cells (legacy bootstrap
352 // path).
353#ifdef ESPRESSO_CALIPER
355 CALI_MARK_BEGIN("ghost/post");
356#endif
357 for (std::size_t i = 0; i < n; ++i) {
358 auto const &nc = plan.neighbors[i];
359 bufs.recv[i].resize(
360 calc_transmit_size(as_span(bufs.recv_cells[i]), box, data_parts));
361 bufs.requests.push_back(comm.irecv(nc.peer, tag, bufs.recv[i].data(),
362 static_cast<int>(bufs.recv[i].size())));
363 }
364 for (std::size_t i = 0; i < n; ++i) {
365 auto const &nc = plan.neighbors[i];
366 bufs.requests.push_back(comm.isend(nc.peer, tag, bufs.send[i].data(),
367 static_cast<int>(bufs.send[i].size())));
368 }
369
370 // BONDS (cold, resort-only path): a second per-neighbor message for the bond
371 // buffers, mirroring the legacy two-transfer. Kept entirely off the hot
372 // POSITION/FORCE path via the flag guard. boost::mpi serializes the
373 // std::vector<char> length + payload, so recv sizing is automatic here.
374 if (bonds) {
375 for (std::size_t i = 0; i < n; ++i) {
376 auto const &nc = plan.neighbors[i];
377 bufs.requests.push_back(
378 comm.irecv(nc.peer, TAG_BONDS, bufs.recv[i].bonds()));
379 }
380 for (std::size_t i = 0; i < n; ++i) {
381 auto const &nc = plan.neighbors[i];
382 bufs.requests.push_back(
383 comm.isend(nc.peer, TAG_BONDS, bufs.send[i].bonds()));
384 }
385 }
386#ifdef ESPRESSO_CALIPER
388 CALI_MARK_END("ghost/post");
389#endif
390
391 // Local (same-rank) copies run here, after messages are posted, so they
392 // overlap with in-flight network transfers for split-phase callers.
393 //
394 // Direction:
395 // Push: copy the real (src) cell into its self-ghost (dst), applying the
396 // periodic-image shift so the ghost holds the correct replica.
397 // Reduce: roles swap -- the self-ghost (dst) accumulated a short-range
398 // force contribution during the pair loop that must be added back
399 // into the real (src) cell. Copy dst -> src (no position shift;
400 // GHOSTTRANS_FORCE with ReductionPolicy::UPDATE accumulates via
401 // +=). This mirrors the legacy GHOST_LOCL entry in the reverted
402 // collect-forces communicator; without the swap the self-wrap
403 // force contributions (node_grid[i]==1 periodic axes) are dropped,
404 // which silently biases forces on particles that interact across
405 // the local periodic boundary.
406 //
407 // Correctness (no aliasing with in-flight buffers):
408 // Push: local_cell_copy writes ghost cells; isends carry real-cell data.
409 // Reduce: local_cell_copy writes real (owned) cells; isends carry ghost-
410 // cell data. In both cases the written and sent cell sets are
411 // disjoint. Local copies complete before halo_exchange_finish
412 // unpacks any receive buffer.
413 for (auto const &lc : plan.local) {
414 if (push)
415 local_cell_copy(*lc.src, *lc.dst, lc.shift, box, data_parts);
416 else
417 local_cell_copy(*lc.dst, *lc.src, Utils::Vector3d{}, box, data_parts);
418 }
419
420 return st;
421}
422
424 unsigned data_parts, ExchangeOp op) {
425 // Convenience overload for callers that do not hold a persistent
426 // ExchangeBuffers (unit tests, cold resort paths). Allocate a pool via
427 // unique_ptr so it is freed automatically when the GhostExchange is
428 // destroyed — on every exit path, including the GHOSTTRANS_NONE early return
429 // in halo_exchange_finish. Use the pool overload on hot paths.
430 auto pool = std::make_unique<ExchangeBuffers>();
431 auto st = halo_exchange_start(plan, box, data_parts, op, *pool);
432 // Transfer ownership: owned keeps the pool alive; bufs already points to it.
433 st.owned = std::move(pool);
434 return st;
435}
436
438 if (st.data_parts == GHOSTTRANS_NONE)
439 return;
440
441 auto &bufs = *st.bufs;
442
443 // Local (same-rank) copies were already performed in halo_exchange_start
444 // to overlap with in-flight messages; nothing to do here.
445
446 if (st.plan->collective)
447 run_collective(*st.plan, *st.box, st.data_parts, st.op);
448
449 auto const n = st.plan->neighbors.size();
450 bool const bonds = (st.data_parts & GHOSTTRANS_BONDS) != 0u;
451 bool const add = st.op.combine == Combine::Add;
452
453 // Request layout (set by halo_exchange_start):
454 // [0..n) = per-neighbor irecvs
455 // [n..2n) = per-neighbor isends
456 // [2n..3n) = bond irecvs (only when bonds)
457 // [3n..4n) = bond isends (only when bonds)
458
459 if (bonds) {
460 // Cold path (resort-only): bonds need *both* the flat and bond message from
461 // each neighbor before unpack_cells can run. Keep existing wait_all-then-
462 // unpack-in-order semantics; pipelining is not safe here.
463#ifdef ESPRESSO_CALIPER
465 CALI_MARK_BEGIN("ghost/wait");
466#endif
467 boost::mpi::wait_all(bufs.requests.begin(), bufs.requests.end());
468#ifdef ESPRESSO_CALIPER
470 CALI_MARK_END("ghost/wait");
472 CALI_MARK_BEGIN("ghost/unpack");
473#endif
474 for (std::size_t i = 0; i < n; ++i) {
475 auto dst = as_span(bufs.recv_cells[i]);
476 unpack_cells(bufs.recv[i], dst, *st.box, st.data_parts);
477 }
478#ifdef ESPRESSO_CALIPER
480 CALI_MARK_END("ghost/unpack");
481#endif
482 } else if (add) {
483 // Add path (FORCE / RATTLE reduce): wait receives in *fixed neighbor order*
484 // so that floating-point addition into shared local cells is bitwise
485 // reproducible across runs. Unpacking neighbor i overlaps delivery of
486 // i+1..n-1 (pipelining while preserving associativity).
487 //
488 // Caliper: outer ghost/wait and ghost/unpack markers always fire (even
489 // when n==0) so that label discovery works on single-rank runs.
490 // Per-neighbor inner BEGIN/END pairs interleave with the outer ones;
491 // repeated sequential begin/end accumulates — valid Caliper usage.
492#ifdef ESPRESSO_CALIPER
494 CALI_MARK_BEGIN("ghost/wait");
495#endif
496 for (std::size_t i = 0; i < n; ++i) {
497 bufs.requests[i].wait();
498#ifdef ESPRESSO_CALIPER
500 CALI_MARK_END("ghost/wait");
502 CALI_MARK_BEGIN("ghost/unpack");
503#endif
504 auto dst = as_span(bufs.recv_cells[i]);
505 if (st.data_parts & GHOSTTRANS_FORCE) {
506 add_forces(bufs.recv[i], dst, st.data_parts);
507 }
508#ifdef ESPRESSO_BOND_CONSTRAINT
509 else if (st.data_parts == GHOSTTRANS_RATTLE) {
510 add_rattle(bufs.recv[i], dst);
511 }
512#endif
513#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
514 else if (st.data_parts == GHOSTTRANS_DIPFLD) {
515 add_dip_fld(bufs.recv[i], dst);
516 }
517#endif
518#ifdef ESPRESSO_CALIPER
520 CALI_MARK_END("ghost/unpack");
522 CALI_MARK_BEGIN("ghost/wait");
523#endif
524 }
525 // Wait for sends [n..2n) so buffers are safe to reuse next step.
526 // The outer ghost/wait region (opened before the loop) is still open here
527 // on the n==0 path; on n>0 the loop re-opened it after the last unpack.
528 boost::mpi::wait_all(bufs.requests.begin() + static_cast<std::ptrdiff_t>(n),
529 bufs.requests.begin() +
530 static_cast<std::ptrdiff_t>(2 * n));
531#ifdef ESPRESSO_CALIPER
533 CALI_MARK_END("ghost/wait");
535 CALI_MARK_BEGIN("ghost/unpack");
537 CALI_MARK_END("ghost/unpack");
538#endif
539 } else {
540 // Overwrite path (position/property push): arrival order is safe because
541 // the validator guarantees each ghost cell is filled by exactly one
542 // message. Use wait_any to unpack each neighbor's buffer as soon as it
543 // arrives.
544 //
545 // Bookkeeping: maintain a slot->neighbor index map so we always know which
546 // recv buffer to unpack for the completed request slot. Completed requests
547 // are swapped to the end of the active range and the map is updated in
548 // sync.
549 //
550 // Caliper: outer ghost/wait always fires (even n==0); the wait_any loop
551 // emits nested end/begin pairs; an outer ghost/unpack pair fires after the
552 // loop so the label appears on single-rank runs.
553#ifdef ESPRESSO_CALIPER
555 CALI_MARK_BEGIN("ghost/wait");
556#endif
557 // Initialize the scratch map to the identity: slot i -> neighbor i.
558 // The vector was pre-sized in halo_exchange_start (no allocation here).
559 for (std::size_t i = 0; i < n; ++i)
560 bufs.slot_to_neighbor[i] = i;
561
562 // active_end tracks how many recv requests are still pending.
563 std::size_t active_end = n;
564
565 while (active_end > 0) {
566 auto first = bufs.requests.begin();
567 auto last =
568 bufs.requests.begin() + static_cast<std::ptrdiff_t>(active_end);
569
570 auto [status, done_it] = boost::mpi::wait_any(first, last);
571 (void)status;
572#ifdef ESPRESSO_CALIPER
574 CALI_MARK_END("ghost/wait");
576 CALI_MARK_BEGIN("ghost/unpack");
577#endif
578
579 std::size_t const done_slot =
580 static_cast<std::size_t>(done_it - bufs.requests.begin());
581 std::size_t const neighbor_idx = bufs.slot_to_neighbor[done_slot];
582
583 {
584 auto dst = as_span(bufs.recv_cells[neighbor_idx]);
585 unpack_cells(bufs.recv[neighbor_idx], dst, *st.box, st.data_parts);
586 }
587
588 // Remove the completed slot from the active range by swapping it with
589 // the last active slot and shrinking the range.
590 --active_end;
591 if (done_slot != active_end) {
592 std::iter_swap(done_it, bufs.requests.begin() +
593 static_cast<std::ptrdiff_t>(active_end));
594 std::swap(bufs.slot_to_neighbor[done_slot],
595 bufs.slot_to_neighbor[active_end]);
596 }
597#ifdef ESPRESSO_CALIPER
599 CALI_MARK_END("ghost/unpack");
600 if (espresso_cali_active() && active_end > 0)
601 CALI_MARK_BEGIN("ghost/wait");
602#endif
603 }
604
605#ifdef ESPRESSO_CALIPER
606 // Wait for sends [n..2n) so buffers are safe to reuse next step.
607 // Caliper balance: when n==0 the outer CALI_MARK_BEGIN("ghost/wait") posted
608 // before the loop is still open (the loop body never ran, so nothing closed
609 // it). When n>0 the last loop iteration emitted END("ghost/unpack") and
610 // then — because active_end reached 0 — did NOT re-open "ghost/wait", so
611 // the region is closed. The guard `if (n > 0)` therefore re-opens for
612 // n>0 and is deliberately absent for n==0 (the outer region serves).
613 // Invariant: exactly one "ghost/wait" region is open entering wait_all.
614 if (espresso_cali_active() && n > 0)
615 CALI_MARK_BEGIN("ghost/wait");
616#endif
617 boost::mpi::wait_all(bufs.requests.begin() + static_cast<std::ptrdiff_t>(n),
618 bufs.requests.begin() +
619 static_cast<std::ptrdiff_t>(2 * n));
620#ifdef ESPRESSO_CALIPER
622 CALI_MARK_END("ghost/wait");
624 CALI_MARK_BEGIN("ghost/unpack");
626 CALI_MARK_END("ghost/unpack");
627#endif
628 }
629
630 // st.owned (if non-null) will free the heap-allocated pool automatically
631 // when the GhostExchange goes out of scope after this call returns.
632}
633
634void halo_exchange(HaloPlan const &plan, BoxGeometry const &box,
635 unsigned data_parts, ExchangeOp op, ExchangeBuffers &bufs) {
636 auto st = halo_exchange_start(plan, box, data_parts, op, bufs);
638}
639
640void halo_exchange(HaloPlan const &plan, BoxGeometry const &box,
641 unsigned data_parts, ExchangeOp op) {
642 // Convenience overload: uses the no-pool start, which allocates a temporary
643 // ExchangeBuffers on the heap and frees it in finish.
644 auto st = halo_exchange_start(plan, box, data_parts, op);
646}
647
648} // namespace GhostComm
Asynchronous, split-phase ghost-communication engine.
Zero-overhead Caliper guards for the inactive (no CALI_CONFIG) case.
bool espresso_cali_active() noexcept
Return true if Caliper is configured for this process.
Class that stores marshalled data for ghost communications.
std::size_t size() const
Returns the number of elements in the non-bond storage.
auto & bonds()
Returns a reference to the bond storage.
void resize(std::size_t new_size)
Resizes the underlying storage s.t.
DEVICE_QUALIFIER constexpr bool empty() const noexcept
Definition Array.hpp:164
DEVICE_QUALIFIER constexpr reference front()
Definition Array.hpp:122
Ghost particles and particle exchange.
@ GHOSTTRANS_RATTLE
transfer ParticleRattle
Definition ghosts.hpp:46
@ GHOSTTRANS_DIPFLD
transfer dipole field tracking data
Definition ghosts.hpp:60
@ GHOSTTRANS_PARTNUM
resize the receiver particle arrays to the size of the senders
Definition ghosts.hpp:49
@ GHOSTTRANS_POSITION
transfer ParticlePosition
Definition ghosts.hpp:39
@ GHOSTTRANS_FORCE
transfer ParticleForce
Definition ghosts.hpp:43
@ GHOSTTRANS_NONE
Definition ghosts.hpp:35
@ GHOSTTRANS_TORQUE
transfer torque (reduced with force; runtime-conditional)
Definition ghosts.hpp:56
@ GHOSTTRANS_BONDS
Definition ghosts.hpp:50
std::span< ParticleList *const > as_span(std::vector< ParticleList * > const &v)
View a list of cell pointers as a span for the packing routines.
void run_collective(HaloPlan const &plan, BoxGeometry const &box, unsigned data_parts, ExchangeOp op)
Collective (broadcast/reduce-sum) section.
void pack_regions(CommBuf &buf, std::vector< SendRegion > const &regions, BoxGeometry const &box, unsigned data_parts)
Pack all send regions of one NeighborComm into a single buffer.
GhostTag
MPI tags for ghost communications.
std::vector< ParticleList * > region_cells(std::vector< SendRegion > const &send)
Extract the plain cell pointers from a list of send regions.
void unpack_cells(CommBuf &buf, std::span< ParticleList *const > cells, BoxGeometry const &box_geo, unsigned data_parts)
Unpack particle data from a communication buffer into cells.
void pack_cells(CommBuf &buf, std::span< ParticleList *const > cells, Utils::Vector3d const &shift, BoxGeometry const &box_geo, unsigned data_parts)
Pack particle data from cells into a communication buffer.
void local_cell_copy(ParticleList &src, ParticleList &dst, Utils::Vector3d const &shift, BoxGeometry const &box_geo, unsigned data_parts)
Copy particle data from src to dst applying a ghost shift.
std::size_t calc_transmit_size(BoxGeometry const &box_geo, unsigned data_parts)
Calculate the per-particle transmit size for the given data parts.
void add_forces(CommBuf &buf, std::span< ParticleList *const > cells, unsigned data_parts)
Add forces (and optionally torques) from a communication buffer to particles in cells.
void add_dip_fld(CommBuf &buf, std::span< ParticleList *const > cells)
Add dipole fields from a communication buffer to particles.
void halo_exchange_finish(GhostExchange &st)
Complete a halo exchange: run same-rank copies (overlapping the in-flight messages),...
bool report_violations(std::vector< std::string > const &violations, char const *context)
Print violations to stderr and return whether the list was empty.
void halo_exchange(HaloPlan const &plan, BoxGeometry const &box, unsigned data_parts, ExchangeOp op, ExchangeBuffers &bufs)
Blocking wrapper using a caller-owned buffer pool (no per-call alloc after warm-up).
GhostExchange halo_exchange_start(HaloPlan const &plan, BoxGeometry const &box, unsigned data_parts, ExchangeOp op, ExchangeBuffers &bufs)
Begin a halo exchange using a caller-owned buffer pool.
void add_rattle(CommBuf &buf, std::span< ParticleList *const > cells)
Add rattle corrections from a communication buffer to particles.
std::vector< std::string > validate_halo_plan_symmetry(HaloPlan const &plan)
Cross-rank symmetry check for a HaloPlan.
Reusable particle packing/unpacking for ghost communications.
Persistent per-neighbor buffer pool for halo exchanges.
std::vector< CommBuf > send
Per-neighbor packed send buffers (index-aligned with plan->neighbors).
std::vector< std::size_t > slot_to_neighbor
Scratch index map for the Overwrite (wait_any) path in halo_exchange_finish: maps active request slot...
std::vector< CommBuf > recv
Per-neighbor recv buffers (index-aligned with plan->neighbors).
std::vector< std::vector< ParticleList * > > send_cells
Scratch cell-pointer arrays for the packing routines.
std::vector< std::vector< ParticleList * > > recv_cells
std::vector< boost::mpi::request > requests
Outstanding non-blocking send/recv requests (cleared before each use).
Opaque handle for one in-flight halo exchange.
BoxGeometry const * box
ExchangeBuffers * bufs
Non-owning pointer to the active buffer pool (caller's or owned).
bool symmetry_validated
Lazily-set flag for the once-per-plan symmetry check.
Definition HaloPlan.hpp:75
std::optional< CollectiveSection > collective
Definition HaloPlan.hpp:63
boost::mpi::communicator comm
Definition HaloPlan.hpp:60
std::vector< NeighborComm > neighbors
Definition HaloPlan.hpp:61
std::vector< LocalComm > local
Definition HaloPlan.hpp:62