core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
nbx.hpp
Go to the documentation of this file.
1// core — NBX nonblocking-consensus sparse data exchange.
2//
3// A clean reimplementation of the async engine in block_decomposer/src/MPISync.hpp
4// (pbs::MPISync::communicateParticleData) using the canonical NBX protocol of Hoefler et al.,
5// "Scalable Communication Protocols for Dynamic Sparse Data Exchange":
6// 1. Post every outgoing message as a nonblocking synchronous send (MPI_Issend).
7// 2. Loop probing/receiving incoming messages; once all local Issends are matched, enter
8// MPI_Ibarrier; keep draining until the barrier completes — then every message has been
9// received globally.
10// MPI_Issend completes only when the matching receive has been posted, which is what makes the
11// barrier a correct global-termination signal without any prior agreement on message counts.
12//
13// This is the right engine for DYNAMIC / SPARSE patterns where neither the neighbour set nor the
14// message sizes are known ahead of time (particle migration, halo-topology setup). For STATIC grid
15// halos use peclet::core::halo::GridHaloTopology's persistent-neighbour path instead.
16#ifndef PECLET_CORE_HALO_NBX_HPP
17#define PECLET_CORE_HALO_NBX_HPP
18
19#include <memory>
20#include <vector>
21
23
24namespace peclet::core::halo {
25
26namespace detail {
44constexpr long kNbxRoundTags = 64;
45constexpr int kNbxFamilies = 128;
46constexpr int kNbxTagBase = 24576;
47static_assert(kNbxTagBase + kNbxFamilies * kNbxRoundTags <= 32768,
48 "NBX round tags must stay within the MPI-guaranteed tag space");
49inline int nbxRoundTag(MPI_Comm comm, int baseTag) {
50 static int keyval = MPI_KEYVAL_INVALID;
53 void* attr = nullptr;
54 int flag = 0;
56 const long round = flag ? reinterpret_cast<long>(attr) : 0;
57 MPI_Comm_set_attr(comm, keyval, reinterpret_cast<void*>(round + 1));
59 return kNbxTagBase + family * static_cast<int>(kNbxRoundTags) +
60 static_cast<int>(round % kNbxRoundTags);
61}
62} // namespace detail
63
64class NbxEngine {
65 public:
66 explicit NbxEngine(MPI_Comm comm = MPI_COMM_WORLD) : comm_(comm) {}
67
68 MPI_Comm comm() const { return comm_; }
69 int rank() const {
70 int r;
71 MPI_Comm_rank(comm_, &r);
72 return r;
73 }
74 int size() const {
75 int s;
76 MPI_Comm_size(comm_, &s);
77 return s;
78 }
79
80 // Drive one full sparse exchange.
81 // packNext(std::vector<char>& out) -> int : fill `out` with the next outgoing message and
82 // return its destination rank; return < 0 when this rank has no more messages to send.
83 // onRecv(int src, std::vector<char>& msg) : handle a received message.
84 //
85 // `baseTag` is the caller's tag FAMILY, not the wire tag: the round uses
86 // detail::nbxRoundTag(comm, baseTag) — a 64-tag block in the reserved range [24576, 32768)
87 // selected by baseTag % 128, rotated by a per-communicator round counter. Consecutive rounds on
88 // one communicator MUST NOT share a tag: a rank that has already observed the Ibarrier complete
89 // goes on to post the next round's Issends, while a neighbour that has not yet observed
90 // completion is still probing — and would receive the next round's message as this round's
91 // (Hoefler et al. §4). Measured on Snellius 2026-09-02: back-to-back topology builds (one per
92 // multigrid level) on a 64-rank sub-communicator lost 0-20 of 26 send partners per rank, hanging
93 // the first exchange. Keep direct point-to-point tags below detail::kNbxTagBase and distinct NBX
94 // call sites' baseTags distinct modulo 128 (see detail::nbxRoundTag).
95 template <typename PackNext, typename OnRecv>
97 const int tag = detail::nbxRoundTag(comm_, baseTag);
98 // 1) Post all sends as synchronous nonblocking.
99 std::vector<std::unique_ptr<std::vector<char>>> sendBufs;
100 std::vector<MPI_Request> sendReqs;
101 for (;;) {
102 auto buf = std::make_unique<std::vector<char>>();
103 int dest = packNext(*buf);
104 if (dest < 0)
105 break;
107 MPI_Issend(buf->data(), static_cast<int>(buf->size()), MPI_BYTE, dest, tag, comm_, &req);
108 sendBufs.push_back(std::move(buf));
109 sendReqs.push_back(req);
110 }
111
112 // 2) NBX consensus loop.
113 bool barrierStarted = false;
114 bool done = false;
116 std::vector<char> recvBuf;
117 while (!done) {
118 int flag = 0;
120 MPI_Iprobe(MPI_ANY_SOURCE, tag, comm_, &flag, &st);
121 if (flag) {
122 int count = 0;
123 MPI_Get_count(&st, MPI_BYTE, &count);
124 recvBuf.resize(count);
125 MPI_Recv(recvBuf.data(), count, MPI_BYTE, st.MPI_SOURCE, st.MPI_TAG, comm_,
127 onRecv(st.MPI_SOURCE, recvBuf);
128 continue; // keep draining before testing for completion
129 }
130 if (!barrierStarted) {
131 int allSent = 0;
132 MPI_Testall(static_cast<int>(sendReqs.size()), sendReqs.data(), &allSent,
134 if (allSent) {
135 MPI_Ibarrier(comm_, &barrierReq);
136 barrierStarted = true;
137 }
138 } else {
139 int bdone = 0;
141 if (bdone)
142 done = true;
143 }
144 }
145 }
146
147 private:
148 MPI_Comm comm_;
149};
150
151} // namespace peclet::core::halo
152
153#endif // PECLET_CORE_HALO_NBX_HPP
MPI_Comm comm() const
Definition nbx.hpp:68
void exchange(PackNext &&packNext, OnRecv &&onRecv, int baseTag=0)
Definition nbx.hpp:96
NbxEngine(MPI_Comm comm=MPI_COMM_WORLD)
Definition nbx.hpp:66
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:70
int MPI_Comm_get_attr(MPI_Comm c, int keyval, void *val, int *flag)
Definition mpi_stub.hpp:95
int MPI_Testall(int, MPI_Request *, int *flag, MPI_Status *)
Definition mpi_stub.hpp:153
int MPI_Comm
Definition mpi_stub.hpp:17
int MPI_Request
Definition mpi_stub.hpp:18
#define MPI_COMM_WORLD
Definition mpi_stub.hpp:27
#define MPI_REQUEST_NULL
Definition mpi_stub.hpp:29
int MPI_Comm_set_attr(MPI_Comm c, int keyval, void *val)
Definition mpi_stub.hpp:103
#define MPI_STATUS_IGNORE
Definition mpi_stub.hpp:31
#define MPI_BYTE
Definition mpi_stub.hpp:38
int MPI_Iprobe(int, int, MPI_Comm, int *flag, MPI_Status *)
Definition mpi_stub.hpp:141
int MPI_Recv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Status *)
Definition mpi_stub.hpp:129
#define MPI_STATUSES_IGNORE
Definition mpi_stub.hpp:32
int MPI_Ibarrier(MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:167
int MPI_Get_count(const MPI_Status *, MPI_Datatype, int *c)
Definition mpi_stub.hpp:145
int MPI_Issend(const void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:138
#define MPI_ANY_SOURCE
Definition mpi_stub.hpp:33
int MPI_Comm_rank(MPI_Comm, int *r)
Definition mpi_stub.hpp:66
#define MPI_COMM_NULL_COPY_FN
Definition mpi_stub.hpp:77
#define MPI_COMM_NULL_DELETE_FN
Definition mpi_stub.hpp:78
int MPI_Comm_create_keyval(MPI_Comm_copy_attr_function *, MPI_Comm_delete_attr_function *, int *keyval, void *)
Definition mpi_stub.hpp:90
int MPI_Test(MPI_Request *, int *flag, MPI_Status *)
Definition mpi_stub.hpp:149
#define MPI_KEYVAL_INVALID
Definition mpi_stub.hpp:79
constexpr int kNbxTagBase
Definition nbx.hpp:46
constexpr long kNbxRoundTags
Per-communicator round counter, kept as an MPI attribute so it lives and dies with the communicator (...
Definition nbx.hpp:44
constexpr int kNbxFamilies
Definition nbx.hpp:45
int nbxRoundTag(MPI_Comm comm, int baseTag)
Definition nbx.hpp:49
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26