DOLFINx 0.12.0.0
DOLFINx C++
Loading...
Searching...
No Matches
Scatterer.h
1// Copyright (C) 2022-2026 Igor Baratta and Garth N. Wells
2//
3// This file is part of DOLFINx (https://www.fenicsproject.org)
4//
5// SPDX-License-Identifier: LGPL-3.0-or-later
6
7#pragma once
8
9#include "IndexMap.h"
10#include "MPI.h"
11#include "sort.h"
12#include <algorithm>
13#include <array>
14#include <cassert>
15#include <cstdint>
16#include <format>
17#include <mpi.h>
18#include <numeric>
19#include <ranges>
20#include <span>
21#include <stdexcept>
22#include <type_traits>
23#include <vector>
24
25namespace dolfinx::common
26{
85template <class Container = std::vector<std::int32_t>>
86class Scatterer
87{
88 static_assert(std::is_integral_v<typename Container::value_type>);
89
90 template <class>
91 friend class Scatterer;
92
93public:
95 using container_type = Container;
96
104 explicit Scatterer(const IndexMap& map)
105 : _sizes_remote(map.src().size(), 0),
106 _displs_remote(map.src().size() + 1), _sizes_local(map.dest().size()),
107 _displs_local(map.dest().size() + 1)
108 {
109 if (dolfinx::MPI::size(map.comm()) == 1)
110 return;
111
112 int ierr;
113 const std::span<const int> src = map.src();
114 const std::span<const int> dest = map.dest();
115
116 // Check that src and dest ranks are unique and sorted
117 assert(std::ranges::is_sorted(src));
118 assert(std::ranges::is_sorted(dest));
119
120 // Create communicators with directed edges:
121 // (0) owner -> ghost,
122 // (1) ghost -> owner
123 MPI_Comm comm0;
124 ierr = MPI_Dist_graph_create_adjacent(
125 map.comm(), src.size(), src.data(), MPI_UNWEIGHTED, dest.size(),
126 dest.data(), MPI_UNWEIGHTED, MPI_INFO_NULL, false, &comm0);
127 _comm0 = dolfinx::MPI::Comm(comm0, false);
128 dolfinx::MPI::check_error(map.comm(), ierr);
129
130 MPI_Comm comm1;
131 ierr = MPI_Dist_graph_create_adjacent(
132 map.comm(), dest.size(), dest.data(), MPI_UNWEIGHTED, src.size(),
133 src.data(), MPI_UNWEIGHTED, MPI_INFO_NULL, false, &comm1);
134 _comm1 = dolfinx::MPI::Comm(comm1, false);
135 dolfinx::MPI::check_error(map.comm(), ierr);
136
137 // Build permutation array that sorts ghost indices by owning rank
138 std::span owners = map.owners();
139 std::vector<std::int32_t> perm(owners.size());
140 std::iota(perm.begin(), perm.end(), 0);
141 dolfinx::radix_sort(perm, [&owners](auto index) { return owners[index]; });
142
143 // Sort (i) ghost indices and (ii) ghost index owners by rank
144 // (using perm array)
145 std::span ghosts = map.ghosts();
146 std::vector<int> owners_sorted(owners.size());
147 std::vector<std::int64_t> ghosts_sorted(owners.size());
148 std::ranges::transform(perm, owners_sorted.begin(),
149 [&owners](auto idx) { return owners[idx]; });
150 std::ranges::transform(perm, ghosts_sorted.begin(),
151 [&ghosts](auto idx) { return ghosts[idx]; });
152
153 // For data associated with ghost indices, packed by owning
154 // (neighbourhood) rank, compute sizes and displacements. I.e., when
155 // sending ghost index data from this rank to the owning ranks,
156 // disp[i] is the first entry in the buffer sent to neighbourhood
157 // rank i, and disp[i + 1] - disp[i] is the number of values sent to
158 // rank i.
159 assert(_sizes_remote.size() == src.size());
160 assert(_displs_remote.size() == src.size() + 1);
161 auto begin = owners_sorted.begin();
162 for (std::size_t i = 0; i < src.size(); i++)
163 {
164 auto upper = std::ranges::upper_bound(begin, owners_sorted.end(), src[i]);
165 std::size_t num_ind = std::ranges::distance(begin, upper);
166 _displs_remote[i + 1] = _displs_remote[i] + num_ind;
167 _sizes_remote[i] = num_ind;
168 begin = upper;
169 }
170
171 // For data associated with owned indices that are ghosted by other
172 // ranks, compute the size and displacement arrays. When sending
173 // data associated with ghost indices to the owner, these size and
174 // displacement arrays are for the receive buffer.
175
176 // Compute sizes and displacements of local data (how many local
177 // elements to be sent/received grouped by neighbors)
178 assert(_sizes_local.size() == dest.size());
179 assert(_displs_local.size() == dest.size() + 1);
180 _sizes_remote.reserve(1); // ensure data is not a nullptr
181 _sizes_local.reserve(1); // ensure data is not a nullptr
182 ierr
183 = MPI_Neighbor_alltoall(_sizes_remote.data(), 1, MPI_INT,
184 _sizes_local.data(), 1, MPI_INT, _comm1.comm());
185 dolfinx::MPI::check_error(_comm1.comm(), ierr);
186
187 std::inclusive_scan(_sizes_local.begin(), _sizes_local.end(),
188 std::next(_displs_local.begin()));
189
190 assert(static_cast<int>(ghosts_sorted.size()) == _displs_remote.back());
191
192 // Send ghost global indices to owning rank, and receive owned
193 // indices that are ghosts on other ranks
194 std::vector<std::int64_t> recv_buffer(_displs_local.back(), 0);
195 ierr = MPI_Neighbor_alltoallv(
196 ghosts_sorted.data(), _sizes_remote.data(), _displs_remote.data(),
197 MPI_INT64_T, recv_buffer.data(), _sizes_local.data(),
198 _displs_local.data(), MPI_INT64_T, _comm1.comm());
199 dolfinx::MPI::check_error(_comm1.comm(), ierr);
200
201 const std::array<std::int64_t, 2> range = map.local_range();
202#ifndef NDEBUG
203 // Check that all received indices are within the owned range
204 std::ranges::for_each(recv_buffer, [range](auto idx)
205 { assert(idx >= range[0] and idx < range[1]); });
206#endif
207
208 // Sizes, displacements and indices are all in blocks. The block
209 // size enters only through the MPI datatype used to send them, and
210 // through the caller's pack/unpack.
211 {
212 // Convert the received indices from global to local numbering
213 std::vector<typename container_type::value_type> idx(recv_buffer.size());
214 std::ranges::transform(recv_buffer, idx.begin(),
215 [offset = range[0]](auto i) ->
216 typename container_type::value_type
217 { return i - offset; });
218 _local_inds = std::move(idx);
219 }
220
221 _remote_inds = container_type(perm.begin(), perm.end());
222 }
223
240 template <class U>
241 Scatterer(const Scatterer<U>& s)
242 : _comm0(s._comm0), _comm1(s._comm1),
243 _remote_inds(s._remote_inds.begin(), s._remote_inds.end()),
244 _sizes_remote(s._sizes_remote), _displs_remote(s._displs_remote),
245 _local_inds(s._local_inds.begin(), s._local_inds.end()),
246 _sizes_local(s._sizes_local), _displs_local(s._displs_local)
247 {
248 }
249
254 Scatterer(const Scatterer& scatterer) = default;
255
260 Scatterer(Scatterer&& scatterer) = default;
261
265 ~Scatterer() = default;
266
267 // Copy assignment (deleted). dolfinx::MPI::Comm cannot be copied into
268 // an existing object.
269 Scatterer& operator=(const Scatterer& scatterer) = delete;
270
275 Scatterer& operator=(Scatterer&& scatterer) = default;
276
320 template <typename T>
321 void scatter_fwd_begin_dtype(const T* send_buffer, T* recv_buffer,
322 MPI_Datatype type, MPI_Request& request) const
323 {
324 if (!has_neighbours())
325 {
326 request = MPI_REQUEST_NULL;
327 return;
328 }
329
330 int ierr = MPI_Ineighbor_alltoallv(
331 send_buffer, _sizes_local.data(), _displs_local.data(), type,
332 recv_buffer, _sizes_remote.data(), _displs_remote.data(), type,
333 _comm0.comm(), &request);
334 dolfinx::MPI::check_error(_comm0.comm(), ierr);
335 }
336
358 template <typename T>
359 void scatter_fwd_begin(const T* send_buffer, T* recv_buffer, int bs,
360 MPI_Request& request) const
361 {
362 // Checked here too, to avoid building a datatype that will not be
363 // used
364 if (!has_neighbours())
365 {
366 request = MPI_REQUEST_NULL;
367 return;
368 }
369
371 scatter_fwd_begin_dtype(send_buffer, recv_buffer, type.type(), request);
372 }
373
386 void scatter_fwd_end(MPI_Request& request) const
387 {
388 if (!has_neighbours())
389 return;
390
391 wait(_comm0, request);
392 }
393
437 template <typename T>
438 void scatter_rev_begin_dtype(const T* send_buffer, T* recv_buffer,
439 MPI_Datatype type, MPI_Request& request) const
440 {
441 if (!has_neighbours())
442 {
443 request = MPI_REQUEST_NULL;
444 return;
445 }
446
447 int ierr = MPI_Ineighbor_alltoallv(
448 send_buffer, _sizes_remote.data(), _displs_remote.data(), type,
449 recv_buffer, _sizes_local.data(), _displs_local.data(), type,
450 _comm1.comm(), &request);
451 dolfinx::MPI::check_error(_comm1.comm(), ierr);
452 }
453
475 template <typename T>
476 void scatter_rev_begin(const T* send_buffer, T* recv_buffer, int bs,
477 MPI_Request& request) const
478 {
479 // Checked here too, to avoid building a datatype that will not be
480 // used
481 if (!has_neighbours())
482 {
483 request = MPI_REQUEST_NULL;
484 return;
485 }
486
488 scatter_rev_begin_dtype(send_buffer, recv_buffer, type.type(), request);
489 }
490
503 void scatter_rev_end(MPI_Request& request) const
504 {
505 if (!has_neighbours())
506 return;
507
508 wait(_comm1, request);
509 }
510
548 const container_type& local_indices_block() const noexcept
549 {
550 return _local_inds;
551 }
552
583 const container_type& remote_indices_block() const noexcept
584 {
585 return _remote_inds;
586 }
587
588private:
589 // False only on a single rank, where _comm0/_comm1 stay MPI_COMM_NULL
590 bool has_neighbours() const noexcept
591 {
592 return _comm0.comm() != MPI_COMM_NULL;
593 }
594
595 // Complete a non-blocking request, checking errors against `comm`
596 static void wait(const dolfinx::MPI::Comm& comm, MPI_Request& request)
597 {
598 int ierr = MPI_Wait(&request, MPI_STATUS_IGNORE);
599 dolfinx::MPI::check_error(comm.comm(), ierr);
600 }
601
602 // Communicator where the source ranks own the indices in the callers
603 // halo, and the destination ranks 'ghost' indices owned by the
604 // caller. I.e.,
605 // - in-edges (src) are from ranks that own my ghosts
606 // - out-edges (dest) go to ranks that 'ghost' my owned indices
607 dolfinx::MPI::Comm _comm0{MPI_COMM_NULL};
608
609 // Communicator where the source ranks have ghost indices that are
610 // owned by the caller, and the destination ranks are the owners of
611 // indices in the callers halo region. I.e.,
612 // - in-edges (src) are from ranks that 'ghost' my owned indices
613 // - out-edges (dest) are to the owning ranks of my ghost indices
614 dolfinx::MPI::Comm _comm1{MPI_COMM_NULL};
615
616 // Permutation indices used to pack and unpack ghost data (remote)
617 container_type _remote_inds;
618
619 // Number of remote indices (ghosts) for each neighbor process
620 std::vector<int> _sizes_remote;
621
622 // Displacements of remote data for mpi scatter and gather
623 std::vector<int> _displs_remote;
624
625 // Permutation indices used to pack and unpack local shared data
626 // (owned indices that are shared with other processes). Indices are
627 // grouped by neighbor process.
628 container_type _local_inds;
629
630 // Number of local shared indices per neighbor process
631 std::vector<int> _sizes_local;
632
633 // Displacements of local data for mpi scatter and gather
634 std::vector<int> _displs_local;
635};
636
655template <class Container>
656std::vector<std::int32_t>
658 std::span<const std::int32_t> indices, int bs)
659{
660 if (bs < 1)
661 {
662 throw std::invalid_argument(
663 std::format("Block size must be positive, not {}.", bs));
664 }
665
666 const std::int32_t size_local = map.size_local();
667 const std::int32_t num_blocks = size_local + map.num_ghosts();
668
669 // Throw before the scatters, which all ranks must reach
670 for (std::int32_t i : indices)
671 {
672 if (i < 0 or i / bs >= num_blocks)
673 throw std::out_of_range("Local index out of range.");
674 }
675
676 // Initialised to 1 for this rank. The owner holds one
677 // local_indices_block entry per ghosting rank, so it counts
678 // occurrences rather than accumulating a reverse scatter.
679 std::vector<std::int32_t> count(num_blocks, 1);
680 const Container& local = sc.local_indices_block();
681 const Container& remote = sc.remote_indices_block();
682 for (std::int32_t i : local)
683 ++count[i];
684
685 std::vector<std::int32_t> send(local.size());
686 std::ranges::transform(local, send.begin(),
687 [&count](std::int32_t i) { return count[i]; });
688 std::vector<std::int32_t> recv(remote.size());
689 MPI_Request request = MPI_REQUEST_NULL;
690 sc.scatter_fwd_begin(send.data(), recv.data(), 1, request);
691 sc.scatter_fwd_end(request);
692 for (std::size_t i = 0; i < remote.size(); ++i)
693 count[size_local + remote[i]] = recv[i];
694
695 std::vector<std::int32_t> n(indices.size());
696 std::ranges::transform(indices, n.begin(), [&count, bs](std::int32_t i)
697 { return count[i / bs]; });
698 return n;
699}
700} // namespace dolfinx::common
A duplicate MPI communicator and manage lifetime of the communicator.
Definition MPI.h:47
MPI_Comm comm() const noexcept
Return the underlying MPI_Comm object.
Definition MPI.cpp:71
An MPI datatype for count contiguous values of type T, and manage its lifetime.
Definition MPI.h:346
MPI_Datatype type() const noexcept
The datatype to pass to MPI.
Definition MPI.h:395
Distribution of a global index range [0, N) across MPI ranks.
Definition IndexMap.h:191
std::int32_t num_ghosts() const noexcept
Return the number of ghost indices.
Definition IndexMap.cpp:1076
std::span< const int > dest() const noexcept
Return sorted unique ranks that ghost entries owned by the caller.
Definition IndexMap.cpp:1381
std::array< std::int64_t, 2 > local_range() const noexcept
Return the global range of owned indices.
Definition IndexMap.cpp:1071
std::span< const int > owners() const noexcept
Return ranks that own ghost entries.
Definition IndexMap.h:364
std::int32_t size_local() const noexcept
Return the number of owned indices.
Definition IndexMap.cpp:1081
std::span< const int > src() const noexcept
Return sorted unique ranks that own the caller's ghosts.
Definition IndexMap.cpp:1379
std::span< const std::int64_t > ghosts() const noexcept
Return global indices of ghosts in local ghost-index order.
Definition IndexMap.cpp:1088
MPI_Comm comm() const
Return the communicator that the map is defined on.
Definition IndexMap.cpp:1170
A Scatterer supports the scattering and gathering of distributed data that is associated with a commo...
Definition Scatterer.h:87
Scatterer(const IndexMap &map)
Create a scatterer for data with a layout described by an IndexMap.
Definition Scatterer.h:104
void scatter_rev_begin_dtype(const T *send_buffer, T *recv_buffer, MPI_Datatype type, MPI_Request &request) const
Start a non-blocking neighbourhood collective exchange of ghost data with the owning ranks.
Definition Scatterer.h:438
void scatter_fwd_begin(const T *send_buffer, T *recv_buffer, int bs, MPI_Request &request) const
Start a non-blocking neighbourhood collective exchange of owned data with the ranks that ghost it.
Definition Scatterer.h:359
Scatterer(Scatterer &&scatterer)=default
void scatter_rev_end(MPI_Request &request) const
Complete a non-blocking MPI neighbourhood collective send.
Definition Scatterer.h:503
const container_type & local_indices_block() const noexcept
Array of indices for packing/unpacking owned data to/from a send/receive buffer.
Definition Scatterer.h:548
void scatter_fwd_begin_dtype(const T *send_buffer, T *recv_buffer, MPI_Datatype type, MPI_Request &request) const
Start a non-blocking neighbourhood collective exchange of owned data with the ranks that ghost it.
Definition Scatterer.h:321
Scatterer & operator=(Scatterer &&scatterer)=default
void scatter_rev_begin(const T *send_buffer, T *recv_buffer, int bs, MPI_Request &request) const
Start a non-blocking neighbourhood collective exchange of ghost data with the owning ranks.
Definition Scatterer.h:476
Container container_type
Container type used to store local and remote indices.
Definition Scatterer.h:95
void scatter_fwd_end(MPI_Request &request) const
Complete a non-blocking MPI neighbourhood collective send.
Definition Scatterer.h:386
const container_type & remote_indices_block() const noexcept
Array of indices for packing/unpacking ghost data to/from a send/receive buffer.
Definition Scatterer.h:583
Scatterer(const Scatterer< U > &s)
Cast-copy constructor.
Definition Scatterer.h:241
Scatterer(const Scatterer &scatterer)=default
void check_error(MPI_Comm comm, int code) noexcept
Check MPI error code. If the error code is not equal to MPI_SUCCESS, then std::abort is called.
Definition MPI.cpp:89
int size(MPI_Comm comm)
Definition MPI.cpp:81
Miscellaneous classes, functions and types.
Definition dolfinx_common.h:8
std::vector< std::int32_t > num_sharing_ranks(const IndexMap &map, std::span< const std::int32_t > indices, int bs)
Number of ranks that share each of the given local indices.
Definition IndexMap.cpp:933
constexpr void radix_sort(R &&range, P proj={})
Sort a range with radix sorting algorithm. The bucket size is determined by the number of bits to sor...
Definition sort.h:81